一个Java类从被加载到虚拟机内存到被卸载出内存为止,生命周期一共包括如下几个阶段:
- 加载(Loading)
- 验证 (Verfication)
- 准备 (Preparation)
- 解析 (Resolution)
- 初始化 (Initialization)
- 使用 (Using)
- 卸载 (Unloading)
其中验证、准备、解析这个3个部分统称为链接(Linking)。
加载、验证、准备、初始化和卸载这5个阶段开始执行的顺序是一定的,但不意味着这几个阶段是分开执行的,这些阶段通常是相互交叉混合式进行的,通常会在一个阶段执行的过程中调用、激活另一个过程。
解析阶段则不一定,为了支持java的运行时绑定,在某些特定的情况下解析可以在初始化阶段之后开始。
加载阶段
这个阶段主要完成如下3件事情
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流代表的静态存储结构转化成方法区的运行时数据结构
- 生成一个此类的Java.lang.Class对象(方法区中),作为方法区这个类的数据的访问入口。
这里获取字节流的方式并不局限于zip,还包括诸如网络中获取、运行时生成、其他文件生成、数据库读取等方式。
同时相对于类加载过程的其他阶段,相对于一个非数组类的加载阶段是开发者可控性最强的,因为加载阶段既可以使用系统提供的加载器,也可以用户自定义类加载器来完成类的加载。
数组类的加载情况有所不同,虽然数组类是JVM直接创建的,但是数组的组件,最终还是要依靠类加载器去加载,一个数组类创建主要有如下几点:
- 如果数组的组件类型为引用类型,数组将会在组件的类加载器上被标识。
- 如果组件类型不是引用类型,JVM会将数据将会与引导类加载器关联。
- 数组的可见性与它的组件类型保持一致,如果组件的类型不是引用类型,则数据可见性默认为public。
加载阶段完成后,类的二进制字节流将按照JVM所需的格式存储在方法区中,同时在内存中实例化一个java.lang.Class的实例对象,作为程序访问方法区中这些类数据的外部接口。相对于HotSpot,这个实例对象比较特殊,虽然是一个对象,但并没有放置在堆中,而是放置在方法区中。
验证阶段
这个阶段主要是确保加载的Class文件中的字节流包含的信息符合当前虚拟机的要求,同时不存在损害JVM的行为.
从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证
文件格式验证
这一阶段主要验证字节流是否符合Class文件格式的规范:
- 验证开头的4字节的Magic Number 是否是0xCAFEBABE
- 检验当前的主、次版本号能否能被当前虚拟机处理
- 常量池中是否有不被支持的常量类型
- 指向常量的各种索引值是否有不存在或者不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
- Class文件的各个部分以及文件本身是否被删除或者附带其他信息
以及等等一些步骤,通过这个步骤的验证之后,字节流才会进入方法区中存储,后续的3个阶段的验证,都是基于方法区的存储结构进行的
元数据验证
这一阶段主要是对字节码描述的信息进行语义分析:
- 是否有父类(只有Object没有父类)
- 它的父类是否继承了不允许被继承的类(被final修饰的类)
- 非抽象类是否实现了父类要求实现的方法
- 成员变量、方法等是否与父类相矛盾(覆盖父类final字段,重载的方法是否符合要求)
等等
字节码验证
这一阶段将对类的方法体进行校验分析,保证类的方法在运行时不会做出危害虚拟机安全的事件。这个阶段是整个验证过程中最复杂的一个阶段
- 保证任意时刻操作数栈的数据类型都能和指令代码序列配合工作
- 保证跳转指令不会跳转到方法体之外的指令
- 保证方法体中的类型转换是有效的
等等
没有通过字节码验证的方法体一定是有问题的,但是通过字节码验证的方法体也不能保证一定没问题。
符号引用验证
这个验证发生在JVM将符合引用转换成直接引用的时候,在链接的第三阶段---解析阶段发生
- 符合引用通过字符串描述的全限定名能否找到对应的类
- 指定类中是否存在符合方法的字段描述以及简单名称所描素的方法和字段
- 符号引用的类、字段、方法是否能被当前类访问
等等
验证阶段对于JVM的类加载机制是非常重要的,但不是必要的,因为对程序运行期没有影响
准备阶段
这个阶段会执行类的<clint>(),主要是为类变量(也就是被static修饰的)分配内存并赋值零值的阶段。所谓的零值就是默认的初始值,每种数据类型各有不同的零值(需要注意的是同时被static final 修饰的变量在此时就赋值完毕,并不会存在赋零值的操作)。类的<clint>是编译器自动收集类变量以及静态代码块后自动合并生成的。
- <clint>()对于类和接口来说这个方法并不是必须的。
- <clint>()中,静态语句只能访问定义在它之前定义的静态变量,定义在它之后的静态变量,可以赋值,但不能访问。
- 子类<clint>()不需要显示的调用父类的构造器,JVM保证子类的<clint>()执行之前,父类的<clint>()已经执行完毕。
- 由于父类的<clint>()先执行,所以父类的静态语句优先与子类的静态语句执行
- 先对类,接口的执行<clint>()时并不需要执行父接口的<clint>()方法,只有使用父接口定义的变量时,父接口才会初始化。接口的实现类初始化时也不会调用接口的<clint>()
- JVM保证一个类的<clint>()执行时线程安全的,多线程执行类的<clint>()时只能有一个被执行,其余线程等待(执行完毕后其他线程不再进入<clint>())。如果一个类的<clint>()执行耗时操作,可能会造成多进程阻塞
举个栗子:
public static int abc = 123;
public static final int ABC = 123;
上面两句的代码在准备阶段的区别是
- 准备阶段过后,变量abc的初始值是0,而不是123。被static修饰的变量赋值操作,被JVM收集后存在于类构造器的<Clinit>()中,这里还没执行到类的初始化阶段,所以并不会被赋值成123;
- 同时被static final 修饰的变量ABC在编辑阶段javac就会为ABC生成ConstantValue属性,在准备JVM就会根据ConstantValue直接赋值,也就是准备阶段后ABC的值为123;
解析阶段
这个阶段主要是将字节码文件中的符号引用转化为直接引用,这个阶段发生的时间段并不确定,某些情况下解析可以发生在初始化阶段之后,这是为了支持Java语言的运行时绑定。JVM可以根据需求来判断到底要在类被加载器加载的时候就对常量池中的符号引用进行解析,还是等到另一个符号引用将要被使用的时候去解析。
- 符号引用:符合引用是以一组符号来描述所引用的目标,符号可以可是以任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用的对象并不一定加载到内存中
- 直接引用:直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。相对于符号引用,被直接引用的对象一定已经加载到内存中。
解析主要针对类或接口、字段、类方法、类接口、接口方法、方法类型、方法句柄和调用点限定符7种符号引用,这里主要讲前面4种。
类或接口的解析
假设在类D中要把一个未解析过的符号引用N解析为对一个类或接口C的直接引用,JVM有如下操作:
- C是非数组类型,JVM通过N代表的全限定名传递给D的类加载器去加载C。此过程中可能出去C继承的父类或实现的接口的加载
- C是数组类型,同时元素类型是引用类型,通过上面步骤去加载数组元素的引用类型,并由JVM生成一个代表数组C维度和元素的数组对象
- 解析完成之后进行符号引用验证,确认D对C的引用权限
字段解析
解析一个字段符号引用,会先对字段所属的类或者接口的符号引用先进行解析。解析到所属的类或接口C后JVM按照如下规范对字段进行搜索:
- 若C中包含了简单名称和字段描述都符合的字段,则返回这个字段的直接引用
- 若C中实现了接口,则按照继承关系从下往上搜索各个接口及其父接口,如匹配到简单名称和字段描述都一致的字段,则返回该字段的直接引用
- 若C不是java.lang.Object,按照继承关系从下往上搜索父类,如匹配到简单名称和字段描述都一致的字段,则返回该字段的直接引用
- 查找失败,抛出java.lang.NoSuchFieldError
- 解析完成后对字段进行权限验证
类方法解析
与字段解析一样,解析一个类方法也是要先解析到这个方法所属的类或者接口。解析到所属的类或接口C后JVM按照如下规范对字段进行搜索:
- 若解析发现C是一个接口,则抛出异常
- 剩余步骤和字段解析一致,不再赘述
接口方法解析
与类方法解析相比较:
- 若解析返现C是一个类,则抛出异常
- 由于接口中所有的方法都是public的,省去权限校验这一步骤
初始化阶段
类的初始化是类加载过程的最后一步,到了初始化阶段才开始真正执行java代码
编译器自动收集实例变量初始化以及实例代码块后自动合并生成类的<init>()
子类初始化时会先调用父类<init>(),用以保证子类能正常初始化。
执行子类的<init>()
使用阶段
这个没啥好说的,只要代表类的Class对象还能被引用到,类就还在使用当中。
卸载阶段
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。类的卸载,其实就是方法区中的类的回收。判断一个类是否可以进行回收,需要同时满足下面3个条件:
- 该类所有的实例对象都已经被回收,java堆中不存在任何该类的实例对象
- 加载该类的ClassLoader已经被回收
- 该类的java.lang.Class对象没有在任何地方被引用到,无法在任何地方通过反射访问到该类的方法。
上面条件仅仅说明该类可以进行回收,但是并不想类的实例对象一样,不使用了就立马进行回收。
如上图所示,当左侧所有的引用都消失时,类可以被回收。
Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。由用户自定义的类加载器加载的类是可以被卸载的。
参考书籍
本文摘录、整理自周志明的《深入理解Java虚拟机》一书,如想获得更详细介绍可自己查阅此书。