本文是在学习周志明先生《深入理解 Java 虚拟机》一书时所作的总结笔记,在此对周先生表示诚挚的感谢。
1. 概述
虚拟机的类加载机制:
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
2. 类加载的时机
类被加载的整个生命周期
加载 Loading --> 验证 Verification --> 准备 Preparation --> 解析 Resolution --> 初始化 Initialization --> 使用 Using --> 卸载 Unloading
其中加载,验证,准备,初始化和卸载这 5 个阶段的顺序是确定的。
由于在初始化之前,必须要先完成类加载的过程,并且在虚拟机规范中严格规定了有且只有 5 种情况必须对类进行初始化,因此这些情况下也必须对类加载。
这 5 种情况分别为
- 遇到 new、getstatic、putstatic、invokestatic 这4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。场景如下
- 使用 new 关键字实例化对象的时候
- 读取 getstatic 或者设置 putstatic 一个类的静态字段时
- 调用一个类的静态方法时
- 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化
- 当触发一个类的时候,如果发现父类还没有进行过初始化,则需要先触发其父类的初始化
- 当 jvm 启动时,用户需要指定一个要执行的主类,也就是包含 main 方法的哪个类,jvm 会先初始化这个主类
- java.lang.invoke.MethodHandle
这里引入了两个关键词,主动引用和被动引用。
- 主动引用: 上面的场景就称为对一个类的主动引用
- 被动引用: 除了上面的场景,所有引用的方法都不会触发初始化,称为被动引用
关于主动引用和被动引用的测验请看下面例子(为了集中展示,作者将这几个 class 的定义都放在了一起,如果读者需要调试运行,请将其拆开,放在不同的文件下)
public class SuperClass { static { System.out.println("SuperClass init"); } public static int value = 123; } public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); } }
其中 value 是在 SuperClass 中定义的静态变量,当在运行时只有定义到该字段的类才能初始化。
在本例中,subClass 不会初始化,因此其输出应该为
而如果将 value 值的定义挪到 SubClass 中
public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } public static int value = 123; }
得到的结果为
super 和 sub class 都初始化了,这是因为在主动引用中有一条:
当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
因此 subClass 在初始化时,会将其父类 SuperClass 也一并初始化。
此时如果将 NotInitialization 的 main 方法修改如下
package com.reycg.jvm; public class NotInitialization { public static void main(String[] args) { SuperClass[] superClassArray = new SuperClass[10]; } }
在运行时会发现程序没有输出,也就是说没有触发 SuperClass 的初始化。
使用 javap 来查看 NotInitialization 会获取main 方法如下
main 方法触发了另外一个名为 [Lcom/reycg/jvm/SuperClass 的类的初始化,它是一个由虚拟机自动生成的,直接继承自 java.lang.Object 的子类,创建动作由 newarray 触发。
继续看第 3 个被动引用的例子,定义一个 ConstClass 如下
package com.reycg.jvm; public class ConstClass { static { System.out.println("ConstClass init"); } public static final String HELLOWORLD = "helloworld"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } }
运行得到的结果为
可见,在运行时并没有初始化 ConstClass, 这是因为 HELLOWORLD 的值在编译阶段经过常量传播优化,会存储在 NotInitialization 类的常量池中,之后 NotInitialization 对常量 ConstClass.HELLOWORLD 的引用实际都转化为了 NotInitialization 常量池的引用。
这一点我们可以通过使用 javap 命令对 NotInitialization 的字节码计算看出
3. 类加载的过程
这部分是对类加载的全过程进行详细的学习。
3.1 加载
在加载阶段,jvm 需要完成下面 3 件事情
- 通过一个类的全限定名来获取定义此 class 的二进制字节流
- 将该字节流代表的静态存储结构转化为方法区的运行时结构数据
- 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
在加载完成后,jvm 外部的 class 的二进制字节流 就会存储在方法区,也就是 HotSpot 的永久代中。对 HotSpot 来说,接下来就会在方法区,也就是永久代中实例化一个 java.lang.Class 类的对象。这个对象就会作为程序访问方法区中的这些类型数据的外部接口。
3.2 验证
验证的目的是为了确保 Class 文件的字节流中包含的信息符合 jvm 的要求,并且不会危害 jvm 自身的安全。
从整体来看,验证阶段大致会完成下面 4 个阶段的检验动作
3.2.1 文件格式验证
这一阶段会直接对 class 字节流进行操作,只有通过这部分验证,才能进入内存的方法区中存储。因此后面的 3 个验证阶段全部基于方法区的存储结构进行,不会再操作字节流。
3.2.2 元数据验证
3.2.3 字节码验证
3.2.4 符号引用验证
对虚拟机的类加载机制来说,验证阶段是一个非常重要,但不是一定必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短 jvm 类加载的时间。
3.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。所谓类变量都是添加 static 修饰符的变量。
需要注意以下几点
- 类变量使用的内存在方法区中分配
- 为类变量所赋的初始值,“通常情况下”是数据类型的零值。
假设一个类变量的定义为
public static int value = 123;
那么在准备阶段过后,其初始值就是 0。而将 value 赋值为 123 的动作是在类构造器 <clint>() 方法中进行的。
但是对于 static final 同时修饰的基本类型或者 String 类型的数据,也就是通常所说的 ConstantValue 属性,在准备阶段会直接赋值。
3.4 解析
所谓解析就是 jvm 将符号引用替换为直接引用的过程。
- 符号引用就是一组字面量,它与内存布局无关
- 直接引用可以理解为指针,偏移量,或者能够定位到目标的句柄。它和内存布局直接相关。
通过上面的定义可看出,所谓解析就是将 class 文件中的如常量,变量,方法等在机器内存中存放的过程。
解析动作主要分为下面 4 种
- 类或者接口的解析
- 字段解析
- 接口方法解析
- 类方法解析
3.5 初始化
这个阶段才开始真正执行类中定义的 Java 程序代码,其实就是执行类构造器 <clint>() 方法的过程。
<clint>() 类构造器是怎么产生的呢?
在定义类中,如果包含 static 变量或者语句块,编译器就会收集这些语句,并对这些语句进行合并,从而产生类构造器方法。
既然 <clint>() 与 static 变量直接相关,如果类或者接口中没有 static 变量,也就不会有 <clint>() 方法。
父类,子类 <clint>() 类构造器的执行顺序?
父类的 <clint>() 一定会比子类的 <clint>() 方法先执行。映射到 java 类中,也就是说父类的静态语句一定比子类的静态语句先执行。
<clint> 在多线程环境中需要加锁来确保类构造函数只初始化一次。