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

0、前言

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

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

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

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、使用阶段

分为主动使用和被动使用两大类,二者区别在于被动使用的情况下,类只会进行加载而不会进行初始化。

(TODO:主动使用、被动使用的情况,添加一个思维导图进行分类)

1.5、卸载阶段

和 GC 垃圾回收相关

2、用于理解类生命周期的测试案例

2.1、不考虑父子类继承的情况

案例一:认识 <clinit><init>

Java 源代码

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> 方法的字节码

c 复制代码
// 成员方法的第一个形参是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

输出结果

txt 复制代码
<clinit>...
ClassLifeCycleTest01 main...
<init>...1
<init>...2
<init>...1
<init>...2

总结

  • <init> 方法(实例对象初始化)的逻辑:
    1. 构造器方法作为入口
    2. 先执行super()
    3. 再执行构造方法外部的代码逻辑(顺序拼接)
    4. 最后执行构造方法内部的代码逻辑
  • <clinit> 方法是存在线程安全问题的,但虚拟机会对这个过程加锁,不需要程序员处理

案例二:强化理解 <clinit><init> 的生成逻辑

Java 源代码

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> 方法的字节码

c 复制代码
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> 方法的字节码

c 复制代码
//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

输出结果

txt 复制代码
ClassLifeCycleTest02 clinit ...
20
100
ClassLifeCycleTest02 init ...
ClassLifeCycleTest02 constructor ...
30

总结

  • super() 调用的不是父类的构造器,而是父类的 <init> 方法

  • <clinit> 方法和 <init> 方法的生成逻辑是相同的,区别在于前者针对类变量 ,后者针对成员变量

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

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

    java 复制代码
    static{
    	classVar = 10;
    }
    static int classVar = 20;
    java 复制代码
    // 变量声明提前
    static int classVar = 0;
    static{
    	// 顺序添加原来代码块和显式赋值的过程
    	classVar = 10;
    	classVar = 20;
    }

2.2、考虑父子类继承的情况

案例三:隐藏的 super() 就是调用父类的 <init> 方法

java 复制代码
/**
 * 特别事项:和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> 的字节码

c 复制代码
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 的字节码

c 复制代码
// 注意这里的类变量,是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

输出结果

txt 复制代码
ClassLifeCycleTest02 clinit ...
ClassLifeCycleTest03 clinit ...
20
100
ClassLifeCycleTest02 init ...
ClassLifeCycleTest02 constructor ...
ClassLifeCycleTest03 init ...
ClassLifeCycleTest03 constructor ...
-30

总结

  • 父类优先于子类(初始化、类加载)

案例四:类的被动使用(调用父类的静态变量)

在案例三中,我们直接在 ClassLifeCycleTest03 这个类中的 main 方法进行测试,而 main 方法被调用会默认去加载当前类,因此会丢失掉一些现象。因此,我们额外定义一个 Main 类来作为测试入口

java 复制代码
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);

    }

}

输出结果

txt 复制代码
ClassLifeCycleTest02 clinit ...
20
100
txt 复制代码
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" 等)或常量

    java 复制代码
    // 常量: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 源代码

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 代码

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> 方法。

java 复制代码
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)
相关推荐
学点东西吧.32 分钟前
JVM(五、垃圾回收器)
jvm
请你打开电视看看3 小时前
Jvm知识点
jvm
程序猿进阶4 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
阿龟在奔跑15 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list
王佑辉16 小时前
【jvm】方法区常用参数有哪些
jvm
王佑辉16 小时前
【jvm】HotSpot中方法区的演进
jvm
Domain-zhuo16 小时前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
Theodore_10222 天前
7 设计模式原则之合成复用原则
java·开发语言·jvm·设计模式·java-ee·合成复用原则
我是苏苏2 天前
Web开发:ORM框架之使用Freesql的DbFrist封装常见功能
java·前端·jvm
天草二十六_简村人2 天前
Java语言编程,通过阿里云mongo数据库监控实现数据库的连接池优化
java·jvm·数据库·mongodb·阿里云·微服务·云计算