【JVM系列笔记】类生命周期

61 阅读5分钟

类的生命周期描述了一个类加载、使用、卸载的整个过程。整体可以分为:加载、连接、初始化、使用、卸载。其中连接又分为验证、准备、解析三个子阶段。

1. 加载

加载(Loading)阶段分三步

  1. 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,程序员可以使用Java代码拓展的不同的渠道,如本地磁盘获取,CGLIB动态代理生成,Applet技术网络获取。
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中,方法区中生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
  3. Java虚拟机同时会在堆上生成与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。

为什么不直接从方法区加载信息,节约资源呢?

对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。

这样Java虚拟机就能很好地控制开发者访问数据的范围。

2. 连接

连接阶段分为三个子阶段:

  • 验证,验证内容是否满足《Java虚拟机规范》。
  • 准备,给静态变量赋初值。
  • 解析,将常量池中的符号引用替换成指向内存的直接引用。

2.1. 验证

验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。主要包含如下四部分。

  1. 文件格式验证,文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求。
  2. 元信息验证,例如类必须有父类(super不能为空)。
  3. 验证程序执行指令的语义,比如方法内的指令执行中跳转到不正确的位置。
  4. 符号引用验证,例如是否访问了其他类中private的方法等。

JDK1版本号为45,JDK8版本号为52;

主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过。

2.2. 准备

准备阶段为静态变量(static)分配内存并设置初值。

只讨论JDK8及之后的版本

数据类型初始值
int0
long0L
short0
char'\u0000'
byte0
booleanfalse
double0.0
引用数据类型null

在准备阶段会为value分配内存并赋初值为0,在初始化阶段才会将值修改为1。

final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。

2.3. 解析

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

  • 符号引用就是在字节码文件中使用编号来访问常量池中的内容。
  • 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。

3. 初始化

初始化阶段会执行字节码文件中clinit(class init 类的初始化)方法的字节码指令,包含了静态代码块中的代码,并为静态变量赋值。

3.1. 案例


public class Demo1 {

    public static int value = 1;
    static {
        value = 2;
    }
   
    public static void main(String[] args) {

    }
}

==============================================
iconst_1
putstatic #2 <init/Demo1.value : I>
iconst_2
putstatic #2 <init/Demo1.value : I>
return

1. iconst_1,将常量1放入操作数栈
2.putstatic指令会将操作数栈上的数弹出来,并放入堆中静态变量的位置,
字节码指令中#2指向了常量池中的静态变量value,在解析阶段会被替换成变量的地址。
3.后两步操作类似,执行value=2,将堆上的value赋值为2。
res:value = 2
==============================================
public class Demo1 {
    static {
        value = 2;
    }
    public static int value = 1;
    public static void main(String[] args) {

    }
}
==============================================
iconst_2
putstatic #2 <init/Demo1.value : I>
iconst_1
putstatic #2 <init/Demo1.value : I>
return

res:value = 1

此时注意代码块,变量以及方法都为静态

  • init方法,会在对象初始化时执行
  • main方法,主方法
  • clinit方法,类的初始化阶段执行

3.2. 类初始化

clinit执行

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。final在连接准备阶段就已经赋值,所以不会初始化。
  2. 调用Class.forName(String className)。
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。
  5. final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。

client不会执行

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
  4. 数组的创建不会导致数组中元素的类进行初始化。

3.3. 面试题

/*
如下代码的输出结果是什么?
*/
public class Test1 {
    public static void main(String[] args) {
        System.out.println("A");
        new Test1();
        new Test1();
    }

    public Test1(){
        System.out.println("B");
    }

    {
        System.out.println("C");
    }

    static {
        System.out.println("D");
    }
}

D
A
C
B
C
B

//代码块在每个方法前都会加载
public class Demo01 {
    public static void main(String[] args) {
        System.out.println(A02.a);
        System.out.println(B02.a);
        new B02();
        System.out.println(B02.a);
    }
}

class A02{
    static int a = 0;
    static {
        a = 1;
    }
}

class B02 extends A02{
    static {
        a = 2;
    }
}

1
1
2

//只访问父类静态变量,只初始化父类
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的静态代码块运行");
    }
}

//数组的创建不会导致数组中元素的类进行初始化。
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方法进行初始化。