文章目录
- 概述
- 类的生命周期
- 类加载的时机
- 类加载的主要 5 个阶段
- 加载
- 验证
- 准备
- 准备阶段初始值的含义
- 解析
- 符号引用
- 直接引用
- 解析阶段的理解
- 静态绑定与动态绑定
- 初始化
- 类加载器
- 类加载器与类之间的关系
- 类加载器的种类
- 双亲委派机制
- 双亲委派机制设计目的
- 破坏双亲委派机制
- 破坏双亲委派机制的方法
- 破坏双亲委派机制的意义
概述
JVM
类加载机制,指的是把某个类加载到 JVM
中的整个流程。
类的生命周期
类从被加载到 JVM
中开始,到使用完毕卸载出内存为止,整个生命周期包括:
其中,验证,准备,解析三个部分统称为连接阶段
类加载的时机
在一个类尚未被加载到 JVM
中时,在遇见以下几种情况时,必须立即对类进行加载:
- 创建对象实例,访问类的静态字段或者静态方法时
- 对类进行反射操作时
- 当加载一个类时,如果发现其父类还没有加载,则需要先加载其父类
- 当虚拟机启动时,需要立即加载要执行的主类(包含
main()
方法,要被执行的类)
类加载的主要 5 个阶段
加载
加载阶段,JVM
需要执行的操作主要有:
- 通过一个类的全限定名来获取此类的二进制字节流(来源可能是
.class
文件,有可能是jar
包,也有可能是运行时计算生成的,如动态代理生成的代理类二进制字节流) - 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆中生成一个代表这个类的
Class
对象,作为上一步中方法区的运行时数据结构的访问入口
验证
验证阶段的主要目的,是为了确保 Class
文件的二进制字节流中包含的信息是否符合当前虚拟机的要求,并且不会有危害虚拟机的行为。
验证阶段的逻辑策略是虚拟机保护自身安全的重要手段
验证阶段主要会完成以下四个阶段的检验动作:
- 文件格式验证,如验证字节流的版本是否能被当前虚拟机处理
- 元数据验证,如校验是否继承了
final
修饰的类 - 字节码验证,如校验跳转指令是否正确
- 符号引用验证,验证符号引用中描述的全限定名能否找到对应的类
准备
准备阶段是 JVM
为类变量(静态变量)分配内存并设置类变量初始值的阶段,这些类变量所使用的内存都将在方法区中进行分配。
注意,这里说的是给类变量设置初始值,实例变量(非类变量)将会在对象实例化时随着实例化好的对象一起分配在堆中。
划重点:准备阶段是设置类变量(静态变量)初始值的阶段。
准备阶段初始值的含义
初始值,需要分为常量变量和非常量变量(是否被 final
关键字修饰)
- 非常量变量(未被
final
修饰),则初始值为对应类型的零值(例如char
类型的零值为 `\u0000`) - 常量变量(被
final
修饰),则初始值为代码中指定的值- 例如:
private static final long NUMBER = 888L
,准备阶段将会给NUMBER
变量赋初始值为888L
- 例如:
解析
解析阶段,是 JVM
将常量池中的符号引用替换为直接引用的过程。
符号引用
符号引用,即代码中代表被引用资源的字符串,例如一个方法的名称,一个类名,或者一个静态变量的名称。
直接引用
直接引用,即可以直接访问到引用目标的指针,相对偏移量,或者一个句柄。
解析阶段的理解
符号引用替换成直接引用,意思就是说把代码中代表被引用资源的字符串,替换成对应的能直接访问到这些被引用资源的资源(指针,相对偏移量或者句柄)。例如:
public static int maxValueOfInt() {//在解析阶段,将会把 Integer.MAX_VALUE 这个符号引用转化成能直接访问到 Integer 类的 MAX_VALUE 静态变量的指针return Integer.MAX_VALUE;
}
静态绑定与动态绑定
在一个类的加载过程中,解析阶段是可以先完成一部分,在类的整个加载过程完成后再继续完成剩余部分的,这是为了支持动态绑定。
在这里,绑定的意思就是把符号引用替换成直接引用的过程,所以
- 静态绑定,即在类加载过程中就可以确定符号引用所代表的被引用资源,所以直接在解析阶段就可以完成符号引用替换成直接引用的过程
- 动态绑定,即需要在程序运行过程中才可以确定符号引用所代表的被引用资源,所以需要在整个类加载过程完成后再进行符号引用替换成直接引用的过程
动态绑定常见于当某个父类或接口类有多个子类或实现类时,JVM
需要在程序时机运行阶段才能确定引用的时哪个子类或实现类
初始化
初始化阶段,是对类从加载到使用阶段前的最后一个阶段,也是执行类构造器 <clinit>()
方法的阶段。
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值操作,以及静态块中的语句合并生成的- 如果一个类中没有静态语句块,也没有类变量,那么可能不会生成
<clinit>()
方法
综上所属,初始化阶段,就是执行 <clinit>()
方法的阶段,就是在执行类中的所有非常量类变量的赋值,以及静态块中语句的过程。
类加载器
通过一个类的全限定名来获取描述该类的二进制字节流,执行这个动作的程序就称为类加载器。
类加载器与类之间的关系
在判定两个类是否相等时,需要满足以下两个条件:
- 全限定名相同
- 类加载器相同,即两个类是由同一个类加载器来加载的
可以看出,对于一个任意的类,都需要由加载它的类加载器和这个类本身来一起共同确定在 JVM
中的唯一性
类加载器的种类
类加载器主要有以下几种:
- 启动类加载器:
Bootstrap Class Loader
,由C++
来实现的,负责加载JRE
的lib
目录下的核心类库,如rt.jar
、charsets.jar
等 - 扩展类加载器:
Extension Class Loader
,由ExtClassLoader
来实现的,负责加载JRE
的ext
目录下的扩展类包 - 应用程序类加载器:
Application Class Loader
,由AppClassLoader
来实现的,负责加载classpath
下的类包 - 自定义类加载器:
User Class Loader
,由用户自行实现,负责加载用户自定义路径下的包
请注意,类加载器之间并没有继承关系,使用的是组合关系
双亲委派机制
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
因此,所有的类加载请求都应该被传送到最顶层的启动类加载器中,只有当父加载器不能完成加载请求时,子加载器才会尝试自己去完成加载。
双亲委派机制设计目的
- 沙箱安全机制:保证核心类库不会被随意加载,可以防止用户破坏核心类库的功能,保证了
JVM
的运行安全 - 避免类的重复加载:可以保证一个类只会被一个类加载器加载,从而不会出现类相同而类加载器不同的情况,避免了类的重复加载
破坏双亲委派机制
如果一个类加载器收到了类加载的请求,它没有委派给父类加载器加载,而是直接尝试自己去完成加载,这就叫破坏了双亲委派机制。
破坏双亲委派机制的方法
自定义一个类加载器,然后重写 loadClass(String name, boolean resolve)
方法,把其中的委派逻辑给干掉,直接调用 findClass()
方法即可破坏双亲委派机制:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();//直接把这一段委派给父类加载器的代码注释掉//即可破坏双亲委派机制
// try {
// if (parent != null) {
// c = parent.loadClass(name, false);
// } else {
// c = findBootstrapClassOrNull(name);
// }
// } catch (ClassNotFoundException e) {
// // ClassNotFoundException thrown if class not found
// // from the non-null parent class loader
// }if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
破坏双亲委派机制的意义
双亲委派机制可以保证相同的类不会被重复加载,但是在某些场景下,相同的类需要以不同版本的形式加载进 JVM
。
例如,同一个 Tomcat
容器中,可能会需要部署多个 Web
应用
- 不同的
Web
应用可能会依赖同一个第三方类库的不同版本 Tomcat
容器本身也需要依赖一些第三方类库,不能跟Web
应用使用的第三方类库混淆,否则很可能会给容器本身带来安全问题
所以这个时候,就需要破坏双亲委派机制,让不同的应用程序自己加载自己所需要的类库。
Tomcat
为不同的 Web
应用都创建了不同的 WebAppClassLoader
,在 WebAppClassLoader
中实现了破坏双亲委派机制的逻辑,使不同应用间所依赖的类库版本互相隔离