java静态初始化死锁本质是jvm隐式阻塞,非传统锁竞争;排查需三步:jstack抓取waiting on java.lang.class线程快照,梳理a→b→a类初始化依赖链,通过holder模式、延迟初始化或提取公共配置切断循环依赖。

用 jstack 定位卡住的初始化线程
运行中执行:
jstack -l
打开 dump 文件,重点搜索:
- WAITING on java.lang.Class(不是 Object 或自定义锁)
-
at xxx.
(即类初始化方法,如 com.A.<clinit>(A.java:12)</clinit>) - 连续出现多个类的
<clinit></clinit>调用,比如 A → B → A
若看到类似:
"Thread-0" #11 ... java.lang.Thread.State: WAITING (on object monitor)at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at com.example.B.
at com.example.A.
说明 A 初始化时触发了 B,而 B 又反过来依赖 A 的某个尚未赋值的静态字段。
识别高危静态初始化写法
很多循环依赖藏在看似安全的代码里,注意以下模式:
-
静态方法调用:如
static final X x = Utils.create();,而create()内部调用了Other.class.getMethods() - 枚举构造器引用外部静态字段:枚举常量实例化会立即触发整个枚举类初始化,若其构造器读取了 Service.XXX,而 Service 又依赖该枚举,就闭环了
-
接口中的 static 方法访问其他类:如
interface Log { static void info() { Logger.log(A.TAG); } },首次调用会触发 A 初始化 - 非编译期常量触发加载:public static final String Y = UUID.randomUUID().toString(); ✅ 触发初始化;public static final String X = "abc"; ❌ 不触发
验证与解耦的关键操作
确认循环链后,优先采用低侵入方式修复:
-
延迟初始化:把
static final Service s = new Service();改为static Supplier<service> s = () -> new Service();</service>,首次s.get()才真正创建 - Holder 模式:用静态内部类封装实例,利用 JVM 对内部类按需初始化的特性
- 提取公共配置类:把互相依赖的字段统一移到一个无依赖的 Config 类中,A 和 B 都只读它
- 避免 static 块中耗时操作:网络请求、文件读取、sleep 等会拉长锁持有时间,放大死锁概率
上线前做轻量级验证
写个并发测试,两个线程分别调用疑似互相依赖的类:
new Thread(() -> Class.forName("A")).start();new Thread(() -> Class.forName("B")).start();
观察是否 hang 住。CI 流程中可集成脚本自动分析 jstack 输出,检测 <clinit></clinit> 是否集中出现在 WAITING 线程栈中。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











