0、前言
文中大量图片来源于 B站 黑马程序员
0.1、类加载子系统在 JVM 中的位置
类加载器负责的事情是:加载、链接、解析
0.2、与类的生命周期相关的虚拟机参数
参数 | 描述 |
---|---|
-XX:+TraceClassLoading | 打印出加载且初始化的类 |
1、类的生命周期
堆上的变量在分配空间的时候隐式设置默认初始值(广义0),其中类变量在准备阶段(Preparation)分配空间,成员变量在使用阶段(Using)分配空间
1.1、加载阶段(懒加载)
懒加载的含义是:并不会加载 jar 包中所有的字节码,使用到才会进行加载
加载阶段流程:
- 通过类的全限定名从某个源位置获取定义此类的二进制字节流(内存)
- 这个字节流被解析转换为方法区的数据结构(InstanceKlass)
- 在堆空间中生成一个代表这个类的 java.lang.Class 对象,java.lang.Class 对象 和 InstanceKlass 对象互相指向。作为方法区中这个类的各种操作的访问入口
static 静态字段在 JDK 8 之后和 java.lang.Class 对象存储在一起,即存放在堆空间中
什么是 InstanceKlass
InstanceKlass 是 Java 类在 JVM 中的一个快照,JVM 将从字节码文件中解析出来的常量池,类字段,类方法等信息存储到 InstanceKlass 中,这样 JVM 在运行期便能通过 InstanceKlass 来获取Java类的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。这也是Java反射机制的基础,不需要创建对象,就可以查看加载类中的方法,属性等等信息。
Class 对象由 class 字节码 + ClassLoader 共同决定,不同的 ClassLoader 加载同一个 class 字节码得到不同的 Class 对象,即 class 字节码不能够唯一确定 Class 对象。
1.2、链接阶段
子阶段 | 描述 |
---|---|
验证 | 校验魔数、版本号等 |
准备 | 为类变量(static)分配内存空间,并设置默认值(0) |
解析 | 将符号引用处理为直接引用 |
1.3、初始化阶段
判断一个自定义的类是否被初始化的方法:在
其余见下面的测试案例
1.4、使用阶段
分为主动使用和被动使用两大类,二者区别在于被动使用的情况下,类只会进行加载而不会进行初始化。
1.5、卸载阶段
和 GC 垃圾回收相关
2、用于理解类生命周期的测试案例
2.1、不考虑父子类继承的情况
案例一:认识 <clinit>
和 <init>
Java 源代码
public class ClassLifeCycleTest01 {
public ClassLifeCycleTest01() {
System.out.println("<init>...2");
}
{
// 在字节码层面,这些非静态代码块最终被添加到构造函数的最前面
System.out.println("<init>...1");
}
static {
System.out.println("<clinit>...");
}
public static void main(String[] args) {
System.out.println("ClassLifeCycleTest01 main...");
new ClassLifeCycleTest01();
new ClassLifeCycleTest01();
}
}
字节码
<init>
方法的字节码
// 成员方法的第一个形参是this(从局部变量表可知),将this压入操作数栈
0 aload_0
// 调用父类(Object)的<init>方法,即构造器方法中隐藏在首行的super()
1 invokespecial #1 <java/lang/Object.<init> : ()V>
// System.out.println("<init>...1"),先执行构造器方法外面的代码
4 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
7 ldc #3 <<init>...1>
9 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
// System.out.println("<init>...2"),再执行构造器方法里面的代码
12 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #5 <<init>...2>
17 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 return
输出结果
<clinit>...
ClassLifeCycleTest01 main...
<init>...1
<init>...2
<init>...1
<init>...2
总结
<init>
方法(实例对象初始化)的逻辑:- 构造器方法作为入口
- 先执行
super()
- 再执行构造方法外部的代码逻辑(顺序拼接)
- 最后执行构造方法内部的代码逻辑
<clinit>
方法是存在线程安全问题的,但虚拟机会对这个过程加锁,不需要程序员处理
案例二:强化理解 <clinit>
和 <init>
的生成逻辑
Java 源代码
/**
* 目的:通过一些类变量或成员变量的赋值,进一步理解类生命周期的过程
* 1. 在变量声明之前的代码块中,该变量只可以作为右值表达式,而不能作为左值
* 2. 等价形式为:
* 2.1、将变量声明在类的最前面,初始化为0,
* 2.2、然后按照再将显式赋值和代码块赋值按照出现顺序,整合为一个init方法或clinit方法
*/
public class ClassLifeCycleTest02 {
// 变量classVar01定义在静态代码块之前
static int classVar01;
static {
System.out.println("ClassLifeCycleTest02 clinit ...");
classVar01 = 20;
// System.out.println(classVar01);//正常
classVar02 = 10;
// classVar02 = classVar01 + 1; //正常,classVar02可以作为右值
// classVar02 = classVar02 + 1; //异常,classVar01不可以作为左值
// System.out.println(classVar02);//异常
}
// 变量classVar02定义在静态代码块之后
static int classVar02 = 100;
public ClassLifeCycleTest02() {
System.out.println("ClassLifeCycleTest02 constructor ...");
instanceVar = 30;
}
{
System.out.println("ClassLifeCycleTest02 init ...");
instanceVar = 10;
// instanceVar = instanceVar + 2;//异常
// System.out.println(instanceVar);//异常
}
// instanceVar的值变化过程: 0->10->20->30
private int instanceVar = 20;
public static void main(String[] args) {
int var01 = ClassLifeCycleTest02.classVar01;
int var02 = ClassLifeCycleTest02.classVar02;
System.out.println(var01);
System.out.println(var02);
ClassLifeCycleTest02 demo = new ClassLifeCycleTest02();
int var = demo.instanceVar;
System.out.println(var);
}
}
字节码
<clinit>
方法的字节码
0 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #12 <ClassLifeCycleTest02 clinit ...>
5 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
//classVar01 = 20
8 bipush 20
10 putstatic #7 <org/example/lifecycle/ClassLifeCycleTest02.classVar01 : I>
//classVar02 = 10
13 bipush 10
15 putstatic #8 <org/example/lifecycle/ClassLifeCycleTest02.classVar02 : I>
// classVar02 = 100
18 bipush 100
20 putstatic #8 <org/example/lifecycle/ClassLifeCycleTest02.classVar02 : I>
23 return
<init>
方法的字节码
//1、将this压入操作数栈
0 aload_0
//2、调用父类的<init>方法,这里父类是Object
1 invokespecial #1 <java/lang/Object.<init> : ()V>
//3、输出字符串
4 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
7 ldc #3 <ClassLifeCycleTest02 init ...>
9 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
//(成员变量在堆上分配空间时会设置默认初始值0,无法通过字节码体现出来)
//4、this.instanceVar = 10
12 aload_0
13 bipush 10
15 putfield #5 <org/example/lifecycle/ClassLifeCycleTest02.instanceVar : I>
//5、this.instanceVar = 20
18 aload_0
19 bipush 20
21 putfield #5 <org/example/lifecycle/ClassLifeCycleTest02.instanceVar : I>
//6、输出字符串
24 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
27 ldc #6 <ClassLifeCycleTest02 constructor ...>
29 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
//7、this.instanceVar = 30
32 aload_0
33 bipush 30
35 putfield #5 <org/example/lifecycle/ClassLifeCycleTest02.instanceVar : I>
38 return
输出结果
ClassLifeCycleTest02 clinit ...
20
100
ClassLifeCycleTest02 init ...
ClassLifeCycleTest02 constructor ...
30
总结
-
super()
调用的不是父类的构造器,而是父类的<init>
方法 -
<clinit>
方法和<init>
方法的生成逻辑是相同的,区别在于前者针对类变量,后者针对成员变量 -
在变量声明之前的代码块中,如果出现了该变量,那么该变量只能够作为右值表达式,而不能作为左值表达式,例如 classVar02 和 instanceVar 变量
-
针对下面的代码块,可以进行等价处理
static{ classVar = 10; } static int classVar = 20;
// 变量声明提前 static int classVar = 0; static{ // 顺序添加原来代码块和显式赋值的过程 classVar = 10; classVar = 20; }
案例三:验证 <clinit>
方法会被 JVM 加锁
思路:让多个线程并发创建对象,让获取到 <clinit>
方法执行锁的线程在 <clinit>
内部陷入死循环(阻塞),观察其它线程是否能够进入到 <clinit>
方法中
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
/**
* 验证 clinit 方法会被添加上锁
*/
@Slf4j
public class ClassLifeCycleTest07 {
static class Demo {
static {
// 为了通过语法检查, 需要添加if(true){}
if (true) {
// 使用log.error()方便区分
log.error("<clinit>()...");
// 为了将线程阻塞在<clinit>()类初始化过程
// 对比实验就是分别观察while(true){}被注释和未被注释的情况
while (true) {
}
}
}
}
private static final int NUM = 100;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
// 用来体现并发环境
log.info("begin: " + System.currentTimeMillis());
// 触发 Demo 类的初始化
Demo demo = new Demo();
// 用来证明并发环境下,一个线程的Demo的初始化没有完成,锁便不会释放,其它线程无法获取到锁也会被阻塞
log.info("end: " + System.currentTimeMillis());
};
List<Thread> threadList = new ArrayList<>(NUM);
for (int i = 0; i < NUM; i++) {
threadList.add(new Thread(runnable, "thread_" + i));
}
// 启动并发线程
for (int i = 0; i < NUM; i++) {
threadList.get(i).start();
}
// 避免主线程退出
for (int i = 0; i < NUM; i++) {
threadList.get(i).join();
}
}
}
测试结果
对比项 | 实验结果 |
---|---|
while(true){} 被注释 | 所有线程执行完成,程序正常退出。仅有一个线程输出 <clinit> 方法中的内容 |
while(true){} 未被注释 | 程序被阻塞,没有任何一个线程输出 end 时间,同样仅有一个线程输出 <clinit> 方法中的内容 |
2.2、考虑父子类继承的情况
案例四:隐藏的 super()
就是调用父类的 <init>
方法
/**
* 特别事项:和Main进行对比,一种类的被动使用导致类没有执行clinit初始化
*/
public class ClassLifeCycleTest03 extends ClassLifeCycleTest02 {
// 变量classVar01定义在静态代码块之前
static int classVar03;
static {
System.out.println("ClassLifeCycleTest03 clinit ...");
classVar03 = 20;
}
private int instanceVar = -20;
{
System.out.println("ClassLifeCycleTest03 init ...");
}
public ClassLifeCycleTest03() {
System.out.println("ClassLifeCycleTest03 constructor ...");
instanceVar = -30;
}
public static void main(String[] args) {
int var01 = ClassLifeCycleTest03.classVar01;
System.out.println(var01);
int var02 = ClassLifeCycleTest03.classVar02;
System.out.println(var02);
ClassLifeCycleTest03 demo = new ClassLifeCycleTest03();
int var = demo.instanceVar;
System.out.println(var);
}
}
字节码
<init>
的字节码
0 aload_0
// 这里可以清晰看到调用父类的<init>方法,其它部分在之前的案例中已经介绍
1 invokespecial #1 <org/example/lifecycle/ClassLifeCycleTest02.<init> : ()V>
4 aload_0
5 bipush -20
7 putfield #2 <org/example/lifecycle/ClassLifeCycleTest03.instanceVar : I>
10 aload_0
11 bipush -30
13 putfield #2 <org/example/lifecycle/ClassLifeCycleTest03.instanceVar : I>
16 return
main
的字节码
// 注意这里的类变量,是ClassLifeCycleTest03.classVar01
0 getstatic #3 <org/example/lifecycle/ClassLifeCycleTest03.classVar01 : I>
3 istore_1
4 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
7 iload_1
8 invokevirtual #5 <java/io/PrintStream.println : (I)V>
11 getstatic #6 <org/example/lifecycle/ClassLifeCycleTest03.classVar02 : I>
14 istore_2
15 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
18 iload_2
19 invokevirtual #5 <java/io/PrintStream.println : (I)V>
// ClassLifeCycleTest03 demo = new ClassLifeCycleTest03()的字节码
// new:分配对象空间,设置广义0值,并将对象地址压入操作数栈
// dup:复制栈顶元素
// invokespecial:调用父类的<init>方法(属于字节码层面的方法)
// 将栈顶元素赋值给demo局部变量
22 new #7 <org/example/lifecycle/ClassLifeCycleTest03>
25 dup
26 invokespecial #8 <org/example/lifecycle/ClassLifeCycleTest03.<init> : ()V>
29 astore_3
30 aload_3
31 getfield #2 <org/example/lifecycle/ClassLifeCycleTest03.instanceVar : I>
34 istore 4
36 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
39 iload 4
41 invokevirtual #5 <java/io/PrintStream.println : (I)V>
44 return
输出结果
ClassLifeCycleTest02 clinit ...
ClassLifeCycleTest03 clinit ...
20
100
ClassLifeCycleTest02 init ...
ClassLifeCycleTest02 constructor ...
ClassLifeCycleTest03 init ...
ClassLifeCycleTest03 constructor ...
-30
总结
- 父类优先于子类(初始化、类加载)
案例五:类的被动使用(调用父类的静态变量)
在案例三中,我们直接在 ClassLifeCycleTest03 这个类中的 main 方法进行测试,而 main 方法被调用会默认去加载当前类,因此会丢失掉一些现象。因此,我们额外定义一个 Main 类来作为测试入口
import org.junit.jupiter.api.Test;
public class Main {
/**
* 和ClassLifeCycleTest03类中的main()方法进行对比
*/
@Test
public void compareClassLifeCycleTest03Test01() {
int var01 = ClassLifeCycleTest03.classVar01;
System.out.println(var01);
int var02 = ClassLifeCycleTest03.classVar02;
System.out.println(var02);
}
@Test
public void compareClassLifeCycleTest03Test02() {
int var03 = ClassLifeCycleTest03.classVar03;
System.out.println(var03);
}
}
输出结果
ClassLifeCycleTest02 clinit ...
20
100
ClassLifeCycleTest02 clinit ...
ClassLifeCycleTest03 clinit ...
20
总结
类(Class) | 类变量(static) | 调用示例 | 类加载(Loading) | 类初始化(Initialization) |
---|---|---|---|---|
子类(ClassLifeCycleTest03) | 子类(classVar03) | ClassLifeCycleTest03.classVar03 | 父类、子类 | 父类、子类 |
子类(ClassLifeCycleTest03) | 父类(classVar02) | ClassLifeCycleTest03.classVar02 | 父类、子类 | 父类 |
父类(ClassLifeCycleTest02) | 父类(classVar02) | ClassLifeCycleTest01.classVar02 | 父类 | 父类 |
调用静态方法同上
注:可以通过添加虚拟机参数 -XX:+TraceClassLoading
查看已经加载的类,再通过 Ctrl + f
来搜索某个类是否被加载
2.3、考虑常量的编译期优化
代码中所有对常量的引用,都会在编译后直接被替换为相应的字面量
在 Java 中什么是常量?
- 从字节码角度来看,含有 ConstantValue 信息的字段是常量
- 从 Java 代码角度来看,使用
static final
修饰,且右侧表达式中只包含字面量(1、1.0、“hello” 等)或常量// 常量:static final修饰,右侧只包含字面量 static final int NUM_1 = 100; static final int NUM_2 = 200; // 常量:static final修饰,右侧只包含常量 static final int SUM = NUM_1 + NUM_2; // 字符串同理 static final String S_1 = "HELLO"; static final String S_2 = "WORLD"; static final String S_3 = S_1 + S_2; // 不是常量,右侧出现new对象,这就是static final修饰的变量不一定是常量的原因。 // 其它类型的引用变量必定是new出来的对象,而String类型却有两种赋值方式 static final String S_4 = new String(S_1 + S_2);
Java 源代码
/**
* 常量的编译期优化
*
* 可以通过反编译看出Demo.VAR被替换为字面量"Hello World",因此不会触发 Demo 的加载和初始化
*/
public class ClassLifeCycleTest04 {
static class Demo {
private static final String VAR = "Hello World!";
static {
System.out.println("Demo clinit ...");
}
}
public static void main(String[] args) {
// 在编译期便完成对常量的替换,所以不会加载 Demo.class,更不会初始化。
// 注意这里 main方法并不是 Demo 类的方法
System.out.println(Demo.VAR);
}
}
反编译后的 Java 代码
public class ClassLifeCycleTest04 {
public ClassLifeCycleTest04() {
}
public static void main(String[] args) {
// 可以得出结论,在编译后的字节码文件中,Demo.VAR直接被替换为"Hello World"字面量
System.out.println("Hello World!");
}
static class Demo {
private static final String VAR = "Hello World!";
Demo() {
}
static {
System.out.println("Demo clinit ...");
}
}
}
2.4、验证类变量(static)在准备阶段(Preparation)设置默认值
思路:对类变量只进行声明,而不显式赋值。观察字节码中是否有 <clinit>
方法。
public class ClassLifeCycleTest06 {
static int num;
public static void main(String[] args) {
System.out.println(num);
}
}
2.5、使用 HSDB 工具来判断 static 变量的存储位置
(TODO:添加过程细节)
注意:inspect 找到的是 InstanceKlass 对象
3、补充
- 静态变量(static)的存放位置
- JDK 7 及之前:方法区(InstanceKlass)
- JDK 8 及之后:堆(java.lang.Class)