【JVM | 第三篇】—— 类的生命周期 完整详解

类的生命周期是JVM 虚拟机层面 定义的,描述了一个.class文件从被加载到内存,到最终被卸载出内存的整个过程。它是所有 Java 对象存在的基础,也是理解 Spring、Nacos 等框架底层原理的核心前提。

一、类的生命周期总览

一个类的完整生命周期分为7 个阶段 ,其中前 5 个阶段统称为类加载过程

java 复制代码
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

注意:解析阶段不一定在初始化之前,也可能在初始化之后执行,这是为了支持 Java 的动态绑定(多态)特性。

二、核心阶段详解(类加载过程)

1. 加载阶段(Loading)

核心任务 :找到并读取字节码文件,生成对应的Class对象。

JVM 做了 3 件事:

  1. 通过一个类的全限定名(如cn.qimingxing.UserService)获取定义它的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口

关键细节:

  • 字节流来源 :不仅可以从本地.class文件读取,还可以从网络(如 Nacos 配置中心、Spring Cloud 远程调用)、JAR 包、数据库、动态生成(如 ASM、CGLIB)等地方获取

  • 类加载器 :所有的加载动作都由类加载器 完成,Java 默认提供 3 层类加载器:

    表格

    类加载器 负责加载的类 示例
    启动类加载器(Bootstrap) JAVA_HOME/jre/lib 下的核心类 rt.jar、charsets.jar
    扩展类加载器(Extension) JAVA_HOME/jre/lib/ext 下的扩展类 各种扩展 jar 包
    应用程序类加载器(App) 项目 classpath 下的类 你自己写的代码、第三方依赖
  • 双亲委派模型:类加载时会先委托父类加载器加载,父类加载不了才自己加载。好处是保证核心类的安全性,避免被恶意替换。

2. 验证阶段(Verification)

核心任务:确保字节码符合 JVM 规范,防止恶意代码或错误代码破坏 JVM 的安全。

验证内容:

  1. 文件格式验证 :检查字节码文件是否符合.class文件格式规范(如魔数0xCAFEBABE、版本号等)
  2. 元数据验证:检查类的元数据是否符合 Java 语法规范(如是否有父类、方法签名是否正确等)
  3. 字节码验证:最复杂的阶段,检查字节码指令是否合法(如不会跳转到方法外、不会操作非法内存等)
  4. 符号引用验证:检查符号引用是否能正确解析为直接引用(如引用的类、方法、字段是否存在)

关键细节:

  • 验证阶段是可选的,如果代码已经经过充分测试,可以通过-Xverify:none参数关闭验证,加快类加载速度
  • 这是 JVM 的安全屏障,防止加载恶意字节码文件

3. 准备阶段(Preparation)

核心任务 :为静态变量分配内存,并设置默认初始值

关键规则:

  • 只处理静态变量static修饰),不处理实例变量
  • 只设置默认初始值,不是代码中写的初始值
  • 特殊情况:如果静态变量被final修饰,并且是编译期常量(如static final int a = 10),则在准备阶段就会赋值为代码中的值

示例:

java 复制代码
public class Test {
    // 准备阶段:分配内存,值为0(默认值)
    // 初始化阶段:才会赋值为10
    public static int a = 10;
    
    // 准备阶段:直接赋值为20(final+编译期常量)
    public static final int b = 20;
    
    // 准备阶段:不处理,实例变量在对象实例化时才分配内存
    public int c = 30;
}

4. 解析阶段(Resolution)

核心任务 :将常量池中的符号引用 转换为直接引用

概念解释:

  • 符号引用 :用一组字符串来描述引用的目标,比如Ljava/lang/String;methodName等,和 JVM 的内存布局无关
  • 直接引用:直接指向目标的内存地址指针,和 JVM 的内存布局相关

解析类型:

  • 类或接口的解析
  • 字段的解析
  • 类方法的解析
  • 接口方法的解析

关键细节:

  • 解析阶段可能在初始化之后执行,这是为了支持 Java 的动态绑定(多态)
  • 比如调用一个父类的方法,实际执行的是子类的实现,这就需要在运行时才能确定直接引用

5. 初始化阶段(Initialization)

核心任务 :执行类的静态代码块静态变量的赋值语句,这是类加载过程的最后一步。

执行顺序:

  1. 先执行父类的静态代码块和静态变量赋值
  2. 再执行子类的静态代码块和静态变量赋值
  3. 静态代码块只能访问定义在它之前的静态变量,定义在它之后的静态变量只能赋值,不能访问

触发条件(主动使用):

只有当类被主动使用时,才会触发初始化。Java 规定了 6 种主动使用的情况:

  1. 创建类的实例(new关键字)
  2. 调用类的静态方法
  3. 访问类的静态变量(除了 final 常量)
  4. 通过反射调用类(Class.forName("cn.qimingxing.UserService")
  5. 初始化子类时,会先初始化父类
  6. 启动类(包含main方法的类)

被动使用(不会触发初始化):

  1. 通过子类引用父类的静态变量,只会初始化父类,不会初始化子类
  2. 定义类的数组,不会初始化该类
  3. 引用类的 final 常量,不会初始化该类(因为常量在编译期就已经存入调用类的常量池了)

示例(面试高频考点):

java 复制代码
class Parent {
    public static int a = 10;
    static {
        System.out.println("父类静态代码块");
    }
}

class Child extends Parent {
    public static int b = 20;
    static {
        System.out.println("子类静态代码块");
    }
}

public class Test {
    public static void main(String[] args) {
        // 被动使用:只会输出"父类静态代码块",不会输出子类的
        System.out.println(Child.a);
        
        // 主动使用:会先输出父类,再输出子类
        // System.out.println(Child.b);
    }
}

三、使用阶段(Using)

类初始化完成后,就可以正常使用了。使用阶段分为:

  1. 实例化对象:通过new关键字创建对象,此时会执行:
    • 父类的构造代码块和构造方法
    • 子类的构造代码块和构造方法
  2. 调用对象的方法和属性
  3. 对象的垃圾回收:当对象没有任何引用时,会被 JVM 的垃圾回收器回收

四、卸载阶段(Unloading)

核心任务:将类从方法区中卸载,释放内存。

类卸载的条件(非常苛刻):

  1. 该类的所有实例都已经被回收(堆中没有任何该类的对象)
  2. 加载该类的 ClassLoader 已经被回收
  3. 该类的java.lang.Class对象没有任何地方被引用(没有通过反射访问该类)

关键细节:

  • 普通的应用程序类加载器加载的类,在程序运行期间不会被卸载
  • 只有自定义类加载器加载的类才有可能被卸载
  • Spring Boot 的热部署(DevTools)就是通过自定义类加载器实现的:每次修改代码后,销毁原来的类加载器,创建新的类加载器,重新加载类

五、类的生命周期 vs 对象的生命周期

很多人会混淆这两个概念,这里做一个清晰的对比:

对比维度 类的生命周期 对象的生命周期
管理方 JVM JVM
开始 类被加载到内存 对象被实例化(new
结束 类被卸载出内存 对象被垃圾回收
数量 一个类在 JVM 中只有一个Class对象 一个类可以有无数个对象
执行顺序 先有类的生命周期,后有对象的生命周期 依赖于类的生命周期
静态变量 属于类,在类准备阶段分配内存 不属于对象
实例变量 不属于类 属于对象,在对象实例化时分配内存

六、实战案例:完整演示类的生命周期

java 复制代码
class Parent {
    // 静态变量
    public static int parentStaticVar = 10;
    // 实例变量
    public int parentInstanceVar = 20;

    // 静态代码块
    static {
        System.out.println("1. 父类静态代码块执行");
        System.out.println("   父类静态变量默认值:" + parentStaticVar);
        parentStaticVar = 100;
        System.out.println("   父类静态变量赋值后:" + parentStaticVar);
    }

    // 构造代码块
    {
        System.out.println("3. 父类构造代码块执行");
        System.out.println("   父类实例变量:" + parentInstanceVar);
    }

    // 构造方法
    public Parent() {
        System.out.println("4. 父类构造方法执行");
    }
}

class Child extends Parent {
    public static int childStaticVar = 30;
    public int childInstanceVar = 40;

    static {
        System.out.println("2. 子类静态代码块执行");
        System.out.println("   子类静态变量默认值:" + childStaticVar);
        childStaticVar = 300;
        System.out.println("   子类静态变量赋值后:" + childStaticVar);
    }

    {
        System.out.println("5. 子类构造代码块执行");
        System.out.println("   子类实例变量:" + childInstanceVar);
    }

    public Child() {
        System.out.println("6. 子类构造方法执行");
    }
}

public class LifeCycleTest {
    public static void main(String[] args) {
        System.out.println("0. main方法开始执行");
        new Child();
        System.out.println("7. 对象创建完成");
    }
}
输出结果:
复制代码
0. main方法开始执行
1. 父类静态代码块执行
   父类静态变量默认值:10
   父类静态变量赋值后:100
2. 子类静态代码块执行
   子类静态变量默认值:30
   子类静态变量赋值后:300
3. 父类构造代码块执行
   父类实例变量:20
4. 父类构造方法执行
5. 子类构造代码块执行
   子类实例变量:40
6. 子类构造方法执行
7. 对象创建完成

七、面试必背知识点

  1. 类加载过程分为:加载、验证、准备、解析、初始化
  2. 准备阶段只给静态变量分配内存,设置默认值;final 常量会直接赋值
  3. 初始化阶段执行静态代码块和静态变量赋值,先父类后子类
  4. 只有主动使用才会触发类的初始化,被动使用不会
  5. 类卸载的三个条件:所有实例被回收、ClassLoader 被回收、Class 对象没有引用
相关推荐
light blue bird1 小时前
支轴事件任务线程执行工序路径的图表组件
前端·jvm·windows
fengxin_rou1 小时前
【从零开始的JUC并发第五章】:线程池详解
java·jvm·spring
小江的记录本11 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
思麟呀18 小时前
C++11并发编程:call_once一次性执行+atomic原子类型+CAS无锁编程+自旋锁
linux·开发语言·jvm·c++·windows
Fanfanaas20 小时前
C++ 继承
java·开发语言·jvm·c++·学习·算法
周杰伦fans21 小时前
C# 异常继承深度解析:从设计原则到 sealed 关键字的奥秘
java·jvm·c#
小L写Java1 天前
第三章:Java 内存模型 (JMM) 与运行时数据区
java·jvm
在繁华处1 天前
Java从零到熟练(十):JVM基础与性能优化
java·jvm·性能优化
go不是csgo2 天前
GORM 上手:一个 main.go 跑通 Go 数据库增删改查
jvm·数据库·golang