这是我参与8月更文挑战的第10天,活动详情查看: 8月更文挑战
前言
上一篇JVM 系列聊了JVM的运行时数据区域,但是在进入运行时数据区域之前,Class文件还需要通过类加载机制来进入 JVM;而本文论述的是JVM中类的生命周期
往期推荐:
类在正式被使用前,经历了** 加载,连接,初始化三个阶段**,而连接又分为 验证,准备,解析三个阶段 需要注意的是,这五个阶段虽然是按顺序开始的,但不是按顺序完成的,通常在一个阶段执行的过程中调用或激活另一个阶段,而且解析阶段有可能发生在初始化之后(为了支持Java语言的运行时绑定)
一. 加载阶段
JVM规范定义了在加载阶段,虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取其定义的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为方法区这些数据的访问入口
虚拟机规范上面这 3 点并不具体,因此虚拟机可以灵活实现。比如:”通过一个类的全限定名来获取其定义的二进制字节流” 并没有指明具体从哪里获取、怎样获取
除了数组类型由虚拟机直接负责创建之外,非数组类型在读取二进制字节流的阶段可以由开发人员自定义,我们可以自定义一个类加载器去二进制字节流的获取方式,可以从网络、jar包、本地路径……
二. 连接阶段
2.1 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证完成四个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范,例如:
- 是否以魔数
0xCAFEBABE
开头 - 主次版本号是否在当前虚拟机的处理范围之内
- 常量池中的常量是否有不被支持的类型
- ……
元数据验证:对字节码描述的信息进行语义分析(注意: 对比javac
编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求。例如:
- 这个类是否有父类(除了
java.lang.Object
) - 这个类是否继承了不允继承的类(被final修饰的类)
- 类中的字段、方法是否与父类产生矛盾
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- ……
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
符号引用验证: 确保解析动作能正确执行
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用*
-Xverifynone
*参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
2.2 准备
在准备阶段,正式为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配
对于该阶段有以下几点需要注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、null、false等),而不是被在Java代码中被显式地赋予的值
- 假设一个类变量的定义为:
public static int value = 1
;那么变量value
在准备阶段过后的初始值为0,而不是1,因为这时候尚未开始执行任何Java方法,而把value赋值为1的put static
指令是在程序编译后,存放于类构造器<clinit>()
方法之中的,所以把value
赋值为1的动作将在初始化阶段才会执行
2.3 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用
符号引用(Symbolic References) :符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中
直接引用(Direct References) :直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了
三. 初始化
初始化阶段是执行初始化方法
<clinit>()
方法的过程,这一步 JVM 才开始真正执行类中定义的字节码
对于<clinit> ()
方法:
clinit> ()
方法是线程安全的,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static语句块)中的语句合并生成的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问- 在准备阶段,变量都被赋予了初始值,但是到了初始化阶段,所有变量还要按照用户编写的代码重新初始化。换一个角度,初始化阶段是执行类构造器
<clinit>()
方法的过程
在初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化:
- 当遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条直接码指令时;四条指令分别对应 创建一个实例对象时、程序访问类的静态变量时、程序给静态变量赋值时、程序调用静态方法时 - 使用
java.lang.reflect
包的方法对类进行反射调用时,如果类没有初始化的,会触发初始化 - 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含
main
方法的那个类),虚拟机会先初始化这个类 - 在使用
MethodHandle
和VarHandle
相关代码时,可以看作是轻量级的反射调用机制,要想使用这 2 个调用, 就必须先使用findStaticVarHandle
来初始化要调用的类 - 当一个接口中定义了JDK8新加入的Default方法时,如果有这个接口的实现类发生初始化,那这个接口要先被初始化
JVM初始化的步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
四. 卸载
卸载类即该类的 Class 对象被 GC
类完成生命周期有以下几种情况:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由 JVM 自带的类加载器加载的类是不会被卸载的;但是由我们自定义的类加载器加载的类是可能被卸载的