JVM_类加载子系统
提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!
优秀文章参考:
https://blog.csdn.net/qq_44543508/article/details/102983363
三. 类加载子系统
内存结构(简图):
内存结构(详细图):
1.类加载器及加载过程:
1.1加载过程
加载阶段Loading --> 链接阶段Linking(验证Verification、准备Preparation、解析Resolution) --> 初始化阶段Initialization
- 类加载器子系统负责从文件系统或者网络上加载class文件,class文件在文件开头有特定的文件标识(CA FE BA BE)。
- ClassLoader只负责class文件的加载,至于他是否运行,由Execution Engine(执行引擎)决定。
- 加载的类信息存放于方法区的内存空间(最终成为DNA元数据模板),除了类的信息外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
1.加载阶段(Loading):
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口
2.链接阶段(Linking):
分为三个子阶段:
验证(Verify)
- 确保class文件的字节流包含的信息符合虚拟机要求,保证被加载类的正确性
- 四种验证:格式检查,语义检查,字节码验证,符号引用验证
准备(Prepare)
- 为类静态变量分配内存并且设置该类静态变量的默认初始值,即零值
- 不包含final修饰的static,因为final在编译的时候就分配了,准备阶段会显式的初始化
- 这里不会为实例变量分配初始化,类变量会分配到方法区中,而实例变量是会随着对象一起分配到Java堆中
解析(Resolve)
- 将常量池内的符号引用转化为直接引用的过程
- 事实上,解析操作往往会随着JVM在执行完初始化之后再执行
3.初始化阶段(Initialization):
- 初始化阶段就是执行类构造器方法< clinit >()的过程;
- < clinit >()方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并起来;
- 构造器方法中指令按照语句在源文件中的出现顺序执行;
- < clinit >()不同于类的构造器。(构造器是虚拟机视角下的< init >()方法)
- 若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕;
- 虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁;
1.2类加载器
1.类加载器分类
JVM支持两种类型的类加载器:
- 引导类加载器(也叫启动类加载器Bootstrap Class Loader),由C和C++语言编写
- 自定义类加载器(派生于抽象类ClassLoader的类加载器,包括:
扩展类加载器(Extension Class Loader),
系统类加载器(System Class Loader也叫应用类加载器)
用户自定义类加载器
测试代码:
package com.yuan.classloader;
/**
* @description: 类加载器分类
* @author: ybl
* @create: 2020-09-22 19:56
**/
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
//获取上级:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@1b6d3586
//获取上级:引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); //null
//对于用户自定义的类,默认采用系统类加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2
//String类(Java的核心类库) 默认都是采用的引导类加载器
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); //null
}
}
2.启动类加载器(Bootstrap Class Loader)
- C和C++语言编写,嵌入在JVM
- 加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 不继承ClassLoader,没有父加载器
- 加载扩展类和应用类加载器,并指定为他们的父加载器
- 只加载包名为java、javax、sun等开头的类
3.扩展类加载器(Extension Class Loader)
- Java编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader
- 父加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar包也放在此目录,也会由他加载
测试代码
package com.yuan.classloader;
import sun.misc.Launcher;
import sun.security.ec.CurveDB;
import java.net.URL;
import java.security.Provider;
/**
* @description:自定义类加载器
* @author: ybl
* @create: 2020-09-22 19:26
**/
public class ClassLoaderTest02 {
public static void main(String[] args) {
System.out.println("*********启动类加载器**********");
//获取启动类加载器 能够加载的api路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL.toExternalForm());
}
//从上面目录的jar包中选择一个类,测试其类加载器是什么
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader); //null
System.out.println("*********扩展类加载器**********");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面目录的jar包中选择一个类,测试其类加载器是什么
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1); //sun.misc.Launcher$ExtClassLoader@4b67cf4d
}
}
4.系统类加载器(System Class Loader)
- Java编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader
- 父加载器为扩展类加载器
- 加载环境变量classpath或系统属性java.class.path指定的路径下的类库
- 程序默认的类加载器
- 可通过ClassLoader.getSystemClassLoader()获得
5.用户自定义类加载器
什么时候自定义类加载器?
- 隔离加载类(避免类冲突)
- 修改类的加载方式
- 扩展加载源
- 防止源码泄露
自定义步骤:
2.双亲委派机制
2.1双亲委派机制
工作原理:
- 如果一个类加载器收到请求,它不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终会到达顶层的启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载
2.2双亲委派优势
优势:
- 避免类重复加载
- 保护程序的安全,防止核心的API被随意的篡改
2.3沙箱安全机制
保证对java核心源代码的保护,参照下面代码:
package java.lang;
/**
* @description:
* @author: ybl
* @create: 2020-09-22 20:25
**/
public class String {
public static void main(String[] args) {
System.out.println("aaa");
}
}
//错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
//public static void main(String[] args)
//否则 JavaFX 应用程序类必须扩展javafx.application.Application
3.其他
JVM中表示两个class对象是否为同一类的必要条件?
- 类名必须一样(包含包名)
- 加载这个类的类加载器实例对象必须相同
JVM必须知道一个类型是由启动类加载器加载的还是用户类加载器加载的。如果一个类是用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分存储在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
Java程序对类的使用方式分为:主动使用和被动使用。
主动使用分为八种:(执行了初始化,即< clinit >方法):
- 创建类的实例,如:new、反射、克隆、反序列化
- 访问某个类的静态方法,即使用了invokestatic字节码指令
- 调用类、接口的静态字段(final修饰特殊考虑),如:使用getstatic和putstatic指令(对应访问变量、赋值变量操作)
- 使用java.lang.reflect包中的方法反射类的方法时(eg:Class.forName(“类的全路径”))
- 初始化一个类的子类,如果发现其父类还没有进行初始化过程,则需先触发其父类的初始化(该规则不适用于接口 ,即:一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化)
- 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,都会导致该接口被初始化
- Java虚拟机启动时被标明为启动类的类,如main()方法所在的那个类
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类
除了以上的八种情况,其他Java类的使用方式被看作对类的被动使用(未执行初始化),都不会导致类的初始化。