一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期经历了 7 个阶段:
加载、验证、准备、初始化、卸载,五个阶段的顺序是确定的,解析则不一定。
1. 加载
在加载阶段,Java 虚拟机需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
2. 验证
这一阶段的目的是确保 Class 文件的字节流中包含的信息符合约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 验证阶段主要完成四个阶段的检验动作:
- 文件格式验证,验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理
- 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合要求
- 字节码验证,通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
- 符号引用验证,对类自身以外(常量池种的各种符号引用)的各类信息进行匹配性校验,即该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源
(这个阶段的校验行为发生在虚拟机将符合引用转化为直接引用的时候,这个转化动作将在解析阶段发生。)
3. 准备
正式为类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段。
4. 解析
Java 虚拟机将常量池内的符号引用替换为直接引用的过程,某些情况下可以在初始化阶段之后再开始(Java 中的运行时绑定,也称动态绑定、晚期绑定)。
- 符号引用(Symbolic References):以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。与虚拟机实现的内存布局无关。
- 直接引用(Direct References):可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。和虚拟机实现的内存布局直接相关
对方法或字段的访问,也会在解析阶段中对它们的可访问性(public、protected、private、)进行检查。 对同一个符号引用进行多次解析请求,很常见,虚拟机可对第一次解析的结果进行缓存。
5. 初始化
该阶段是执行类构造器 () 方法的过程,有且只有六种场景必须立即对类进行“初始化”,这六种场景称为对一个类型的主动引用 (():是 Javac 编译器的自动生成物,由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。)
- new、getstatic、putstatic 或 invokestatic 这四条字节码指令,典型的 Java 代码场景有:
- 用 new 关键字实例化对象
- 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)
- 调用过一个类型的静态方法的时候
- 使用 java.lang.reflect 包的方法对类型进行反射调用的时候
- 初始化类的时候,其父类还没有被初始化
- 虚拟机启动时,用户指定的要执行的主类,虚拟机会先初始化该类
- 如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄
- 被 default 关键字修饰的接口方法
除此之外,所有引用类型的方式都不会触发初始化,被称为被动引用,典型场景如下:
- 通过字类引用父类的静态字段,不会导致子类初始化
- 通过数组定义来引用类,不会触发类的初始化
- 常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化