先说结论
类生命周期主要包括加载、链接、初始化、使用和卸载,链接又包含验证、准备和解析。加载创建 Class 对象;准备为静态字段分配内存并设默认值;初始化执行类构造器
<clinit>,按源码顺序处理静态赋值和静态代码块。
JVM 保证同一个类的初始化过程加锁且只执行一次。初始化子类前会先初始化尚未初始化的父类,但使用某个类的编译期常量不一定触发初始化。
关键阶段
验证检查字节码结构和类型安全;解析把常量池符号引用转换为直接引用,具体时机可以延后。加载与链接允许交叉进行,因此不应把流程理解成所有类都严格串行一次完成。
生命周期逐阶段理解
| 阶段 | JVM 的主要工作 | 容易混淆的地方 |
|---|---|---|
| 加载 | 找到 class 字节、创建 Class 对象 | 不等于执行静态代码 |
| 验证 | 校验格式、语义、字节码安全 | 保障类型与指令合法性 |
| 准备 | 为静态字段分配内存、设默认值 | 普通静态赋值尚未执行 |
| 解析 | 符号引用转直接引用 | 可按需延迟进行 |
| 初始化 | 执行 <clinit> |
执行静态字段赋值和 static 块 |
| 卸载 | 回收类元数据 | 依赖类加载器可回收 |
<clinit> 由编译器收集静态变量赋值和静态代码块按源码顺序生成。它不是 Java 源码中可直接调用的方法;若父类尚未初始化,子类初始化前会先初始化父类。
准备与初始化示例
class Config {
static int port = loadPort();
static final int MAX_RETRY = 3;
static { audit("config initialized"); }
}
准备阶段中 port 通常先是 0,MAX_RETRY 这类编译期常量可能直接获得 3。初始化阶段才会执行 loadPort() 和 static 块。若 loadPort 抛异常,类会初始化失败,后续主动使用通常得到 NoClassDefFoundError 等错误链。
哪些操作会触发初始化
new Config(); // 会
Config.staticMethod(); // 会
int p = Config.port; // 读取非编译期常量,会
int n = Config.MAX_RETRY; // 编译期常量通常不会
Config[] array = new Config[10]; // 创建数组类型通常不会
反射、MethodHandle 等路径的具体触发条件要看调用方式。不要把“引用到类名”一概说成初始化,重点是有没有发生规范定义的主动使用。
初始化死锁与副作用
JVM 会保证 <clinit> 在同一类加载器下只由一个线程执行,其他线程等待它完成。静态初始化中做网络请求、获取外部锁、启动回调或相互访问两个类的初始化逻辑,容易造成启动长时间卡住甚至死锁。静态块应只做确定、快速、可恢复的初始化;外部资源放到应用生命周期管理中。
排查类初始化问题
启动失败时先找到最初的 ExceptionInInitializerError cause,而不是只看后续 NoClassDefFoundError。类加载日志、JFR、jcmd VM.classloaders、线程转储可帮助定位是字节缺失、模块访问、加载器隔离还是静态代码阻塞。
初始化时机
常见主动使用包括创建实例、读写非编译期常量的静态字段、调用静态方法、反射调用以及初始化某个主类。数组类型由 JVM 直接创建,不等于初始化其元素类型。
容易踩坑的地方
准备阶段通常只写零值,不执行 static int x = compute();但带 ConstantValue 的编译期常量可能在准备阶段直接赋常量值。类加载完成也不等于已经初始化。
常见问题
追问:类初始化失败后还能再次使用吗?
同一类加载器下初始化失败通常会被标记为错误,后续主动使用会抛出相关错误,不能简单依赖再次执行静态代码恢复。
追问:接口也会初始化吗?
会,但初始化规则与类略有不同。初始化一个类不一定初始化其所有接口,只有使用到接口中需要初始化的成员时才触发相应规则。
追问:为什么 static final String 有时会触发初始化?
字面量拼接等编译期常量会被内联,通常不触发;通过方法计算、new String 或运行时读取配置得到的 final 值不是编译期常量,访问时会触发类初始化。
追问:类卸载的前提是什么?
通常要求该类的定义加载器、该加载器加载的类实例和 Class 对象都不再被强引用,并发生合适 GC。应用类加载器长期存活时,普通业务类一般不会单独卸载。