一文详解JVM中类的生命周期

类的生命周期描述了一个类加载、使用、卸载的整个过程,生命周期如图所示:

01-加载阶段

2、类加载器在 加载完类之后,Java虚拟机会 将字节码中的信息保存到方法区中,方法区 中生成一个 InstanceKlass 对象 ,保存类的所有信息,里边还包含实现特定功能比如 多态的信息(虚方法表)

  • 方法区只是一个虚的概念,具体实现要看虚拟机

3、同时,Java虚拟机会在 上生成与方法区中数据类似的 java.lang.Class对象,作用是 在Java代码中去获取类的信息 以及 存储静态字段的数据(JDK8及之后)

以上就是整个类加载阶段的流程

1.1 思考:为什么需要在方法区和堆区中都创建一份对象

  1. 首先,InstanceKlass 对象 是使用 C++ 编写的对象,Java代码一般不能直接操作C++语言编写的对象;所以Java虚拟机就在堆上创建了一个 java.lang.Class 这种用Java语言包装之后的对象;
  2. java.lang.Class 对象中包含的字段 要少于 方法区中 InstanceKlass 对象的字段

02-连接阶段

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

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

2.1 验证

验证的主要目的是 检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束;

这个阶段一般不需要程序员参与

文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求;

元信息验证,例如类必须有父类(super不能为空)

验证 程序执行指令(字节码指令)的语义是否正确,比如方法内的指令执行中跳转到不正确的位置;

符号引用验证,例如是否访问了其他类中private的方法等

验证案例:版本号的检测

先校验 主版本号,再校验 副版本号

对于JDK版本来说,如果一个运行中的 Java虚拟机的环境 要去加载一个 字节码文件;

那么字节码文件的 主版本号不能高于运行环境主版本号;

如果主版本号相等,字节码文件的副版本号 也不能超过 运行环境的副版本号

2.2 准备

如下代码:

  • 在准备阶段会 为value分配内存并赋初值为0,在 初始化阶段才会将值修改为1
java 复制代码
public class Student{

    public static int value = 1;
}

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

如下例子中,变量加上final进行修饰,在准备阶段value值就直接变成1了,因为final修饰的变量后续不会发生值的变更

2.3 解析

解析阶段 主要是将 常量池中的 符号引用 替换为 直接引用;

符号引用 就是 在字节码文件中使用编号来访问常量池中的内容

直接引用 不再使用编号,而是 使用内存中地址进行访问具体的数据

03-初始化阶段

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

如下代码编译成字节码文件之后,会生成三个方法:

java 复制代码
public class Demo1 {

    public static int value = 1;
    static {
        value = 2;
    }
    public static void main(String[] args) {
    }
}
  • init方法,会在对象初始化时执行;
  • main方法,主方法;
  • clinit方法,类的初始化阶段执行

继续来看clinit方法中的字节码指令:

iconst_1,将常量1放入操作数栈。此时栈中只有1这个数

putstatic指令 会 将操作数栈上的数弹出来,并放入堆中静态变量的位置;

字节码指令中 #2指向了 常量池中的 静态变量value,在 解析阶段 会被替换成 变量的地址

后两步操作类似,执行value=2,将堆上的value赋值为2

3.1 以下几种方式会导致类的初始化

  1. 访问一个类的 静态变量 或者静态方法
    注意:变量是final修饰的 并且 等号右边是常量 不会触发初始化
  2. 调用 Class.forName (String className);
  1. new一个该类的对象时;
  2. 执行 Main() 方法,这个 Main() 方法的当前类一定会被加载并初始化

添加 -XX:+TraceClassLoading 参数可以 打印出加载并初始化的类(+ 代表开启功能的意思)

3.2 clinit 不会执行的几种情况

如下几种情况是不会进行初始化指令执行的:

  • 无静态代码块 且 无静态变量赋值 语句;
  • 有静态变量的声明,但是 没有赋值 语句;
  • 静态变量的定义使用 final 关键字,这类变量会在 准备阶段直接进行初始化

3.3 出现继承情况下的类的初始化

  • 直接访问父类的静态变量,不会触发子类的初始化;
  • 子类的初始化 clinit 调用之前,会先调用父类的 clinit 初始化方法

3.4 练习题

分析如下代码执行结果:

数组的创建不会导致数组中元素的类进行初始化

java 复制代码
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的静态代码块运行");
    }
}

/**
 * 什么都不执行
 */

数组的创建和类的加载是两个不同的概念:

  • 创建数组时,JVM 只会为数组分配内存空间,并不会触发数组元素类型的类加载;
  • 由于没有创建 Test2_A 类的实例,也没有访问 Test2_A 类的静态成员,因此 Test2_A 类不会被加载,静态代码块也不会执行

final修饰的变量 如果 赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化

java 复制代码
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 方法,准备阶段就初始化了;
  • 练习题 这里 等号右边是 Integer.valueOf() 需要在初始化阶段执行

04-类的卸载

判定一个类可以被卸载,需要同时满足下面三个条件:

  1. 此类所有实例对象 都已经 被回收,在堆中不存在任何该类的实例对象以及子类对象;
  1. 加载 该类的 类加载器 已经被回收
  1. 该类对应的java.lang.Class 对象 没有 在任何地方 被引用

05-总结

相关推荐
醇氧1 小时前
java.lang.NumberFormatException: For input string: ““
java·开发语言·spring
TracyCoder1232 小时前
JVM 内存模型全景解析
jvm
sww_10262 小时前
智能问数系统(一):高质量的Text-to-SQL
java·人工智能·ai编程
win x2 小时前
文件操作与io总结
java
洛豳枭薰2 小时前
jvm运行时数据区& Java 内存模型
java·开发语言·jvm
这儿有个昵称2 小时前
互联网大厂Java面试场景:从Spring Boot到微服务架构
java·spring boot·消息队列·微服务架构·大厂面试·数据库优化
填满你的记忆3 小时前
【从零开始——Redis 进化日志|Day5】分布式锁演进史:从 SETNX 到 Redisson 的完美蜕变
java·数据库·redis·分布式·缓存
lendsomething3 小时前
Spring 多数据源事务管理,JPA为例
java·数据库·spring·事务·jpa
nsjqj3 小时前
JavaEE初阶:多线程初阶(2)
java·开发语言