类的生命周期
类从被 JVM 加载到卸载,会经历一套完整生命周期。理解这个过程,有助于分析静态变量初始化、类加载异常、框架启动、SPI、热部署等问题。
# 1. 生命周期阶段
类的生命周期包括:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
其中验证、准备、解析合称为连接。
加载 -> 连接 -> 初始化
├─ 验证
├─ 准备
└─ 解析
# 2. 加载
加载阶段完成三件事:
- 通过类的全限定名获取二进制字节流。
- 把字节流转换为方法区中的运行时数据结构。
- 在堆中生成代表这个类的
java.lang.Class对象。
类的字节流可以来自:
- 本地 class 文件。
- JAR 包。
- 网络。
- 动态生成。
- 数据库或加密文件。
这也是很多框架能够动态增强类的基础。
# 3. 验证
验证阶段确保 Class 文件合法、安全,不会破坏 JVM。
验证内容包括:
- 文件格式验证。
- 元数据验证。
- 字节码验证。
- 符号引用验证。
如果 class 文件被篡改或不符合规范,可能在验证阶段失败。
# 4. 准备
准备阶段为类变量分配内存并设置默认初始值。
public static int value = 100;
准备阶段 value 的值是 0,不是 100。赋值为 100 发生在初始化阶段。
特殊情况:
public static final int VALUE = 100;
如果是编译期常量,可能在准备阶段就有常量值。
# 5. 解析
解析阶段把常量池中的符号引用转换为直接引用。
例如:
- 类或接口解析。
- 字段解析。
- 方法解析。
- 接口方法解析。
解析可以在初始化前完成,也可以在首次使用时按需完成。
# 6. 初始化
初始化阶段执行类构造器 <clinit>()。
<clinit>() 由编译器自动收集:
- 静态变量赋值语句。
- 静态代码块。
static int value = 100;
static {
System.out.println("init");
}
初始化是类加载过程中真正执行业务代码的阶段。
# 7. 类卸载
类卸载需要满足:
- 该类所有实例都被回收。
- 该类对应的
Class对象没有被引用。 - 加载该类的类加载器可以被回收。
普通应用类通常不会频繁卸载。热部署、脚本引擎、插件系统、应用服务器中更容易遇到类卸载和类加载器泄漏问题。
# 8. 生命周期状态图
┌────────┐
│ 加载 │ 读取 class 二进制流,生成 Class 对象
└───┬────┘
▼
┌──────────┐
│ 验证 │ 文件格式、元数据、字节码、安全性校验
└────┬─────┘
▼
┌──────────┐
│ 准备 │ 类变量分配内存,设置默认值
└────┬─────┘
▼
┌──────────┐
│ 解析 │ 符号引用 -> 直接引用
└────┬─────┘
▼
┌──────────┐
│ 初始化 │ 执行 <clinit>()
└────┬─────┘
▼
┌──────────┐
│ 使用 │
└────┬─────┘
▼
┌──────────┐
│ 卸载 │ Class、实例、类加载器都不再可达
└──────────┘
注意:解析阶段不一定严格发生在初始化之前,JVM 可以按需解析。学习时把“加载、连接、初始化”作为主流程即可。
# 9. 准备阶段和初始化阶段的细节
准备阶段只给类变量设置默认值:
public class Demo {
public static int a = 10;
public static final int B = 20;
public static String text = "hello";
}
大致理解:
| 字段 | 准备阶段 | 初始化阶段 |
|---|---|---|
a | 0 | 10 |
B | 可能直接为 20 | 通常无需普通赋值 |
text | null | "hello" |
原因是 static final 基本类型或字符串常量可能在编译期就进入 ConstantValue 属性。
# 10. 什么时候触发初始化
主动使用会触发初始化:
new创建对象。- 读写非编译期常量的静态字段。
- 调用静态方法。
- 反射调用。
- 初始化子类前先初始化父类。
- JVM 启动时初始化主类。
不会触发初始化的常见情况:
public static final int VALUE = 1;
int x = Demo.VALUE; // 编译期常量,通常不会触发 Demo 初始化
通过子类引用父类静态字段,只会初始化真正声明该字段的父类:
class Parent {
static int value = 1;
}
class Child extends Parent {
}
int x = Child.value; // 初始化 Parent,不一定初始化 Child
# 11. 开发中常见问题
# 11.1 静态初始化失败后会怎样
如果类初始化阶段抛出异常,后续再次使用该类可能出现:
NoClassDefFoundError: Could not initialize class
这类问题常见于静态代码块中读取配置、连接外部服务、初始化复杂资源。
# 11.2 为什么不建议静态初始化做重活
静态初始化发生在类初始化阶段,失败后影响整个类使用,而且异常定位不如普通业务方法直观。
建议:
- 静态代码块保持简单。
- 外部资源延迟初始化。
- 初始化失败要有清晰日志。
- 不要在静态初始化中做网络调用。
# 12. Tips 快问快答
Q:类加载和类初始化是一回事吗?
A:不是。加载只是把 class 字节流带入 JVM,初始化才会执行静态变量赋值和静态代码块。
Q:准备阶段静态变量为什么不是代码里写的值?
A:准备阶段设置的是类型默认值,显式赋值通常在初始化阶段执行。
Q:接口初始化规则和类一样吗?
A:不完全一样。初始化接口时通常不要求先初始化父接口,只有真正使用到父接口字段时才触发。
Q:类一定会被卸载吗?
A:不一定。只有类实例、Class 对象和类加载器都不可达时才可能卸载。
# 13. 总结
类加载不是简单地把 class 文件读进内存,而是经历加载、验证、准备、解析、初始化等阶段。面试中最容易混淆的是准备和初始化:准备阶段给静态变量默认值,初始化阶段才执行静态赋值和静态代码块。