类加载子系统之类的生命周期(待完善)

0、前言

文中大量图片来源于 B站 黑马程序员

0.1、类加载子系统在 JVM 中的位置

image-20240429141045222

类加载器负责的事情是:加载、链接、解析

0.2、与类的生命周期相关的虚拟机参数

参数描述
-XX:+TraceClassLoading打印出加载且初始化的类

1、类的生命周期

在这里插入图片描述
在这里插入图片描述

堆上的变量在分配空间的时候隐式设置默认初始值(广义0),其中类变量在准备阶段(Preparation)分配空间,成员变量在使用阶段(Using)分配空间

1.1、加载阶段(懒加载)

懒加载的含义是:并不会加载 jar 包中所有的字节码,使用到才会进行加载

加载阶段流程:

  1. 通过类的全限定名从某个源位置获取定义此类的二进制字节流(内存)
  2. 这个字节流被解析转换为方法区的数据结构(InstanceKlass)
  3. 在堆空间中生成一个代表这个类的 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> 方法(实例对象初始化)的逻辑:
    1. 构造器方法作为入口
    2. 先执行super()
    3. 再执行构造方法外部的代码逻辑(顺序拼接)
    4. 最后执行构造方法内部的代码逻辑
  • <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> 方法的生成逻辑是相同的,区别在于前者针对类变量,后者针对成员变量

  • 在变量声明之前的代码块中,如果出现了该变量,那么该变量只能够作为右值表达式,而不能作为左值表达式,例如 classVar02instanceVar 变量

  • 针对下面的代码块,可以进行等价处理

    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、补充

  1. 静态变量(static)的存放位置
    • JDK 7 及之前:方法区(InstanceKlass)
    • JDK 8 及之后:堆(java.lang.Class)
  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值