一个类的生命周期
一个类的生命周期,有如下七个阶段:
其中加载、验证、准备、初始化和卸载这五个阶段发生的顺序是确定的,但是对于解析阶段来说则不一定,因为它可以发生在初始化阶段之后,这样做是为了支持 Java 语言的运行时绑定特征(也称为动态绑定,大白话就是程序会在运行的时候去选择调用哪个方法)。
加载
一个类什么时候开始加载,虚拟机规范并没有强制进行约束,而是交给虚拟机的具体实现来掌控。
Java 虚拟机的实现都是使用的懒加载(就是什么时候需要用到这个类我才去加载),并不是说一个 jar 文件里面有几百个类,而我只用了其中几个类,我还要把所有的类全部加载进来。当然你可以自己写一个 JVM 这么做,虽然这样做很沙雕。
在加载阶段中,虚拟机要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的类静态存储结构转化为方法区的运行时数据结构。
- 在堆中生成一个代表这个类的 java.lang.Class 对象(这样便可以通过该对象访问步骤2中方法区的那些数据结构)。
连接
验证:
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中的信息符合当前虚拟机要求的规范及安全。从整体上看,验证阶段大致会完成以下4个阶段的检验动作:
文件格式验证:
验证字节流是否符合 Class 文件格式的规范;例如:
- 是否以 0xCAFEBABE 开头
- Class 文件中各个部分及文件本身是否有被篡改
- …
总而言之,这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入 Java 虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区进行的,不会再直接读取、操作字节流了。
元数据验证:
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;例如:
- 这个类是否有父类,因为除了 java.lang.Object 之外,所有的类都有父类
- 这类是否继承了不允许被继承的类(被final 修饰的类)
- …
字节码验证:
这个阶段是整个验证过程中最复杂的一个阶段,主要作用是通过数据流和控制流进行分析,从而确定程序语义是合法的、符合逻辑的,继而保证被校验类的方法在运行时不会作出危害虚拟机的行为,例如:
- 保证任何跳转指令都不会跳转到方法体之外的字节码指令上
- 保证方法体中的类型转换总是有效的
- …
符号引用验证:
主要验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。例如:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述付及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问性(public、private、protected)
- …
符号引用验证的主要目的是确保解析阶段能正常执行。
总结一下:验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证没有问题,那么可以考虑采用 -Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备:
准备阶段是正式为类变量(static 修饰的变量)分配内存并设置类变量初始值的阶段,这些类变量所使用的内存都将在方法区中分配。这个阶段有以下两点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里设置类变量的初始值通常情况下是数据类型默认的零值(如0、0L、null、true),而不是被在Java代码中被显式地赋予的值。举个例子说明:
//定义一个a变量
public static int a = 666;
// 变量a在准备阶段过后的初始值为0,并不是666
// 因为这个时候还没有开始执行任何java方法
// 把a赋值为666是初始化环节要做的事情
这里还需要注意如下几点:
-
对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
-
对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值。
-
对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
-
如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将会根据对应的数据类型而被赋予默认的零值。
-
如果变量同时被 final 和 static 修饰,那么在准备阶段常量就会被初始化为指定的值。我们可以这样理解:这个常量在编译期就将其值放入了调用它的那个类的常量池中。
解析:
解析阶段是虚拟机将常量池内的符号引用(JVM 并不知道引入的其他对象在哪里,所以就用唯一符号来代替)转化为直接引用(内存地址)的过程。
符号引用就是用一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。(其实符号引用和直接引用就好比是key-value的映射关系)
初始化
初始化是类加载过程的最后一个步骤,直到这一阶段虚拟机才真正开始执行类中编写的代码。
初始化就是执行一个class中的static语句和所有类变量的赋值操作(对应字节码就是clinit方法)。
使用
使用阶段包括主动引用(初始化阶段所做的事情就是主动引用)和被动引用,需要注意的是:被动引用不会引起类的初始化。
卸载
一个类什么时候结束生命周期,取决于它的 Class 对象何时结束生命周期。需要注意的是:由 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。
所以类在使用阶段完成后,如果满足下面这些情况,就会被卸载掉:
- 加载该类的类加载器已经被回收了
- jvm 堆中不存在该类的任何实例了
- 该类对应的 Class 对象没有被引用了