文章目录
- 1 JVM虚拟机概述
- 2 类的生命周期
- 2.1 加载阶段
- 2.2 连接阶段
- 2.3 初始化阶段\<client> ★★★★★
- 2.3.1 案例一
- 解析字节码指令
- 2.3.2 案例二
- 2.3.3 小结
- 2.3.4 代码中触发类的初始化的方式
- 2.3.4.0 设置打印出加载并初始化的类
- 2.3.4.1 方式一
- 2.3.4.2 方式二
- 2.3.4.3 方式三
- 2.3.4.4 方式四
- 2.4 使用阶段
- 2.5 卸载阶段
- 附:JDK1.8运行时数据区
- 附:数据类型的初始值表
🙊前言:本文章为瑞_系列专栏之《JVM虚拟机》的类的生命周期篇的初始化阶段小节。由于博主是从B站黑马程序员的《JVM虚拟机》学习其相关知识,所以本系列专栏主要是针对该课程进行笔记总结和拓展,文中的部分原理及图解等也是来源于黑马提供的资料,特此注明。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!
1 JVM虚拟机概述
瑞:请参考《瑞_JVM虚拟机_概述》
2 类的生命周期
类的生命周期描述了一个类加载、使用、卸载的整个过程
类的生命周期一般分为五个阶段:加载 ➡️ 连接 ➡️ 初始化 ➡️ 使用 ➡️ 卸载
瑞:初始化阶段最重要,因为程序员可以干涉
由于连接阶段操作很多,所以,又可以分为七个阶段:加载 ➡️ 验证 ➡️ 准备 ➡️ 解析 ➡️ 初始化 ➡️ 使用 ➡️ 卸载
2.1 加载阶段
瑞:请参考《瑞_JVM虚拟机_类的生命周期》
2.2 连接阶段
瑞:请参考《瑞_JVM虚拟机_类的生命周期》
2.3 初始化阶段<client> ★★★★★
加载 ➡️ 连接 ➡️ 初始化 ➡️ 使用 ➡️ 卸载
瑞:初始化阶段就是执行static代码块赋值
连接阶段中非 final 修饰的静态变量(static)保存的是初始值(上图中 int 数据类型的初始值为 0,点我查看数据类型的初始值表),而且是保存在堆区里面的class对象中。但是最终这个值应该是1(如上图),当这个变量存储的值为1的时候,我们才能拿去使用。而这个赋值为1的操作就是在初始化阶段完成的。
初始化阶段会执行静态代码块中的代码,并为静态变量赋值
初始化阶段会执行字节码文件中 clinit 部分的字节码指令
瑞:clinit可以拆解为: cl —— class 代表类,init代表初始化。即类的初始化
2.3.1 案例一
【案例】静态变量赋值
在下面的案例代码中,声明了一个静态变量 value 赋值为 1 ,在静态代码块中将 value 值赋为 2 ,如下:
public class RayTest {public static int value = 1;static {value = 2;}public static void main(String[] args) {System.out.println("value = " + value);}
}
执行后运行结果如下
value = 2
使用 jclasslib 工具查看字节码文件的方法信息,见下图
瑞:jclasslib 工具的安装可以参考《瑞_JVM虚拟机_概述》
1️⃣ 其中 [0] <init>虽然我们没有写构造方法,但是编译器会帮我们自动生成默认无参构造方法
2️⃣ 其中 [1] main 主方法
3️⃣ 其中 [2] <clinit> 初始化阶段执行
<clinit>方法中的字节码指令如下:
0 iconst_1
1 putstatic #7 <com/ray/onlytest/at2024/t03/t15/RayTest.value : I>
4 iconst_2
5 putstatic #7 <com/ray/onlytest/at2024/t03/t15/RayTest.value : I>
8 return
《Java虚拟机规范》中putstatic
指令是给类中的静态字段赋值,值从操作数栈中获取。iconst_常量值
指令:将常量值放到操作数栈中(临时存放),生成常量。
putstatic
指令说明原文见下图⬇️
瑞:《Java虚拟机规范》官网地址:https://docs.oracle.com/javase/specs/index.html
解析字节码指令
所以字节码指令执行步骤如下
0 iconst_1
1 putstatic #7 <com/ray/onlytest/at2024/t03/t15/RayTest.value : I>
4 iconst_2
5 putstatic #7 <com/ray/onlytest/at2024/t03/t15/RayTest.value : I>
8 return
1️⃣ 编号0行:将常量1放入操作数栈中
2️⃣ 编号1行:将操作数栈中的 1 赋值给常量池中编号为#7(你不一定是#7)的变量即RayTest.value
,由于在连接阶段(准备阶段)中已经对静态变量RayTest.value
分配内存并设置初始值 0 ,所以RayTest.value
由 0 变为了 1
3️⃣ 编号4行:将常量2放入操作数栈中
4️⃣ 编号5行:将操作数栈中的 2 赋值给常量池中编号为#7(你不一定是#7)的变量即RayTest.value
,所以RayTest.value
由 1 变为了 2
2.3.2 案例二
将案例一的两句静态代码对调顺序
public class RayTest {static {value = 2;}public static int value = 1;public static void main(String[] args) {System.out.println("value = " + value);}
}
执行后运行结果如下
value = 1
<clinit>方法中的字节码指令如下:
0 iconst_2
1 putstatic #7 <com/ray/onlytest/at2024/t03/t15/RayTest.value : I>
4 iconst_1
5 putstatic #7 <com/ray/onlytest/at2024/t03/t15/RayTest.value : I>
8 return
连接阶段 value 默认值设为了 0 ,然后在初始化阶段将 2 赋值给 value,再将 1 赋值给 value
2.3.3 小结
clinit方法中字节码指令的执行顺序与Java中编写的顺序是一致的
2.3.4 代码中触发类的初始化的方式
2.3.4.0 设置打印出加载并初始化的类
添加
-XX:+TraceClassLoading
参数可以打印出加载并初始化的类
2.3.4.1 方式一
1️⃣ 访问一个类的静态变量或者静态方法,注意如果这个变量是 final 修饰的并且等号右边是常量则不会触发类的初始化阶段(连接阶段就会直接给 final 修饰的常量赋值)。
public class RayTest {public static void main(String[] args) {int i = RayTest2.i;System.out.println(i);}
}class RayTest2{static {System.out.println("init...");i = 486;}public static int i = 0;
}
运行后发现,RayTest2初始化了(打印了init…)
将上面代码中RayTest2.i
修改为 final 修饰
public class RayTest {public static void main(String[] args) {int i = RayTest2.i;System.out.println(i);}
}class RayTest2{static {System.out.println("init...");
// i = 486;}public static final int i = 486;
}
运行后发现,RayTest2并没有初始化(未打印init…)修改后RayTest2.i
这个变量是 final 修饰的并且等号右边是常量486,则不会触发类的初始化阶段
2.3.4.2 方式二
2️⃣ 调用Class.forName(String className)。
public class RayTest {public static void main(String[] args) throws ClassNotFoundException {Class<?> clazz = Class.forName("com.ray.onlytest.at2023.t12.t05.RayTest2");}
}class RayTest2{static {System.out.println("init...");}
}
2.3.4.3 方式三
3️⃣ new一个该类的对象时。
2.3.4.4 方式四
4️⃣ 执行Main方法的当前类。
2.4 使用阶段
瑞:请参考《瑞_JVM虚拟机_类的生命周期》
2.5 卸载阶段
瑞:请参考《瑞_JVM虚拟机_类的生命周期》
附:JDK1.8运行时数据区
附:数据类型的初始值表
数据类型 | 初始值 |
---|---|
int | 0 |
long | 0L |
short | 0 |
char | ‘\u0000’ |
byte | 0 |
boolean | false |
double | 0.0 |
引用数据类型 | null |
如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏⭐️转发🔗评论📝都是对博主最好的支持~