类的生命周期描述了一个类加载、使用、卸载的整个过程,生命周期如图所示:

01-加载阶段

2、类加载器在 加载完类之后,Java虚拟机会 将字节码中的信息保存到方法区中,方法区 中生成一个 InstanceKlass 对象 ,保存类的所有信息,里边还包含实现特定功能比如 多态的信息(虚方法表)
- 方法区只是一个虚的概念,具体实现要看虚拟机

3、同时,Java虚拟机会在堆 上生成与方法区中数据类似的 java.lang.Class对象,作用是 在Java代码中去获取类的信息 以及 存储静态字段的数据(JDK8及之后)

以上就是整个类加载阶段的流程
1.1 思考:为什么需要在方法区和堆区中都创建一份对象
- 首先,InstanceKlass 对象 是使用 C++ 编写的对象,Java代码一般不能直接操作C++语言编写的对象;所以Java虚拟机就在堆上创建了一个 java.lang.Class 这种用Java语言包装之后的对象;
- java.lang.Class 对象中包含的字段 要少于 方法区中 InstanceKlass 对象的字段

02-连接阶段
连接阶段分为三个子阶段:
- 验证,验证内容是否满足《Java虚拟机规范》;
- 准备,给静态变量赋初值;
- 解析,将常量池中的符号引用替换成指向内存的直接引用

2.1 验证
验证的主要目的是 检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束;
这个阶段一般不需要程序员参与
文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求;

元信息验证,例如类必须有父类(super不能为空)

验证 程序执行指令(字节码指令)的语义是否正确,比如方法内的指令执行中跳转到不正确的位置;

符号引用验证,例如是否访问了其他类中private的方法等
验证案例:版本号的检测
先校验 主版本号,再校验 副版本号

对于JDK版本来说,如果一个运行中的 Java虚拟机的环境 要去加载一个 字节码文件;
那么字节码文件的 主版本号不能高于运行环境主版本号;
如果主版本号相等,字节码文件的副版本号 也不能超过 运行环境的副版本号
2.2 准备


如下代码:
- 在准备阶段会 为value分配内存并赋初值为0,在 初始化阶段才会将值修改为1
java
public class Student{
public static int value = 1;
}
final 修饰 的 基本数据类型 的 静态变量,准备阶段直接会将代码中的值进行赋值。
如下例子中,变量加上final进行修饰,在准备阶段value值就直接变成1了,因为final修饰的变量后续不会发生值的变更

2.3 解析
解析阶段 主要是将 常量池中的 符号引用 替换为 直接引用;
符号引用 就是 在字节码文件中使用编号来访问常量池中的内容

直接引用 不再使用编号,而是 使用内存中地址进行访问具体的数据

03-初始化阶段
初始化阶段会执行字节码文件中 clinit(class init 类的初始化)方法的字节码指令,包含了 静态代码块 中的代码,并为静态变量赋值
如下代码编译成字节码文件之后,会生成三个方法:
java
public class Demo1 {
public static int value = 1;
static {
value = 2;
}
public static void main(String[] args) {
}
}

- init方法,会在对象初始化时执行;
- main方法,主方法;
- clinit方法,类的初始化阶段执行
继续来看clinit方法中的字节码指令:
iconst_1,将常量1放入操作数栈。此时栈中只有1这个数

putstatic指令 会 将操作数栈上的数弹出来,并放入堆中静态变量的位置;
字节码指令中 #2指向了 常量池中的 静态变量value,在 解析阶段 会被替换成 变量的地址

后两步操作类似,执行value=2,将堆上的value赋值为2

3.1 以下几种方式会导致类的初始化
- 访问一个类的 静态变量 或者静态方法 ;
注意:变量是final修饰的 并且 等号右边是常量 不会触发初始化 - 调用 Class.forName (String className);

- new一个该类的对象时;
- 执行 Main() 方法,这个 Main() 方法的当前类一定会被加载并初始化
添加 -XX:+TraceClassLoading 参数可以 打印出加载并初始化的类(+ 代表开启功能的意思)
3.2 clinit 不会执行的几种情况
如下几种情况是不会进行初始化指令执行的:
- 无静态代码块 且 无静态变量赋值 语句;

- 有静态变量的声明,但是 没有赋值 语句;

- 静态变量的定义使用 final 关键字,这类变量会在 准备阶段直接进行初始化

3.3 出现继承情况下的类的初始化
- 直接访问父类的静态变量,不会触发子类的初始化;
- 子类的初始化 clinit 调用之前,会先调用父类的 clinit 初始化方法
3.4 练习题
分析如下代码执行结果:
数组的创建不会导致数组中元素的类进行初始化
java
public class Test2 {
public static void main(String[] args) {
Test2_A[] arr = new Test2_A[10];
}
}
class Test2_A {
static {
System.out.println("Test2 A的静态代码块运行");
}
}
/**
* 什么都不执行
*/
数组的创建和类的加载是两个不同的概念:
- 创建数组时,JVM 只会为数组分配内存空间,并不会触发数组元素类型的类加载;
- 由于没有创建 Test2_A 类的实例,也没有访问 Test2_A 类的静态成员,因此 Test2_A 类不会被加载,静态代码块也不会执行
final修饰的变量 如果 赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化
java
public class Test4 {
public static void main(String[] args) {
System.out.println(Test4_A.a);
}
}
class Test4_A {
public static final int a = Integer.valueOf(1);
static {
System.out.println("Test3 A的静态代码块运行");
}
}
/**
* Test3 A的静态代码块运行
* 1
*/
- final 修饰的字段,等号右边是常量的话不会执行 clinit 方法,准备阶段就初始化了;
- 练习题 这里 等号右边是 Integer.valueOf() 需要在初始化阶段执行
04-类的卸载
判定一个类可以被卸载,需要同时满足下面三个条件:
- 此类所有实例对象 都已经 被回收,在堆中不存在任何该类的实例对象以及子类对象;

- 加载 该类的 类加载器 已经被回收;

- 该类对应的java.lang.Class 对象 没有 在任何地方 被引用

05-总结
