静态初始化块中隐式调用构造方法可能引发jvm类初始化死锁,表现为waiting on java.lang.class、无异常、cpu低迷;需用jstack定位循环链,以system.err.println标记执行流,修复应采用holder模式延迟初始化或切断跨类静态依赖。

静态初始化块中隐式调用构造方法引用(如 static final X x = new X();)可能触发跨类初始化循环,进而引发 JVM 类初始化死锁——这不是普通对象锁竞争,而是 JVM 内部对每个类加的 initialization lock 被互相阻塞所致。问题常表现为启动卡死、无异常、CPU 低迷,线程状态为 WAITING on java.lang.Class。
识别构造引用是否真正触发了类初始化
不是所有 new 都会立即引发死锁,关键看被构造的类是否在初始化过程中反向访问当前类未完成初始化的静态成员:
-
new B()出现在 A 的 static 块中 → 立即触发 B 的<clinit></clinit>执行 - 若 B 的构造器、静态字段或 static 块中访问了 A 的某个静态字段(如
A.FLAG),而该字段在 A 中声明位置靠后(尚未赋值),则读到的是null或默认值 - 若 B 的逻辑依赖该值非空(如用于初始化另一字段),就可能抛出
ExceptionInInitializerError;若未抛错但继续执行并反过来调用 A 的静态方法,则形成等待闭环 - 注意:即使 B 是你写的工具类,只要其构造器里调用了
Logger.getLogger(B.class)或Config.getInstance(),就可能间接触发其他第三方类初始化,埋下跨库循环隐患
用 jstack 定位真实的初始化等待链
别只搜 “deadlock”,重点抓 WAITING on java.lang.Class 和嵌套 <clinit></clinit> 调用:
- 执行
jstack -l <pid></pid>,过滤含java.lang.Class.forName0或ClassLoader.loadClass的线程 - 找连续出现两个及以上不同类的
<clinit></clinit>,例如:
at com.example.A.(A.java:12)
at com.example.B.(B.java:7)
at com.example.A.(A.java:15)
→ 明确构成 A→B→A 循环 - 查看线程锁地址:
waiting to lock (a java.lang.Class for B),再查另一线程是否正持有该地址锁
用 System.err.println 标记真实执行断点
避免 JIT 优化干扰,用 System.err.println 在关键位置打点,还原 JVM 实际执行流:
- 在每个静态变量赋值前加日志:
System.err.println("A: about to new B"); - 在被构造类 B 的构造器第一行、静态块开头也加类似输出
- 运行后观察输出序列,典型死锁信号是:
A: about to new B
B: constructor start
B: trying to read A.CONFIG
A: CONFIG = null ← 卡住 - 特别注意枚举类:每个枚举常量实例化即触发整个枚举类初始化,若其构造器中写了
new Service(),而 Service 又依赖该枚举,立刻闭环
修复:切断构造引用带来的初始化耦合
核心原则是让“构造”行为脱离类加载阶段,转为按需、可控、可降级:
- 把
static final X x = new X();改为延迟 Holder 模式:
private static class Holder { static final X INSTANCE = new X(); }
public static X getInstance() { return Holder.INSTANCE; } - 若必须在类加载时构造,确保被构造类 B 不依赖任何其他类的未就绪静态字段;B 自身应为“纯构造”(无 static 块、无静态字段引用外部类)
- 禁止在构造器中调用日志、配置、反射等高危操作;如需初始化资源,改用实例方法 + 显式 init() 调用
- 对第三方类(如 Jackson 的
ObjectMapper),不直接new在 static 块中,改用 Spring 的@Bean或静态工厂方法封装,由容器管理生命周期










