<clinit> 是类初始化方法,由编译器自动生成,仅含静态变量赋值和静态代码块,按源码顺序合并,在类加载“初始化阶段”执行且线程安全、全局仅一次;<init> 是实例构造方法,每次创建对象时执行,负责实例变量赋值、非静态代码块和构造器逻辑。

两个类在 <clinit> 阶段互相触发初始化,是 JVM 中一种隐蔽但后果严重的死锁场景。它不会抛异常,线程状态常为 BLOCKED 或 WAITING,应用启动卡住、HTTP 请求无响应、后台任务停摆——问题表象各异,根源却高度一致:循环等待类初始化锁。
死锁发生的底层机制
JVM 对每个类的初始化过程施加一个隐式锁(initialization lock),对应其 <clinit> 方法。该方法由编译器自动生成,封装了所有静态变量赋值和 static{} 块逻辑。关键点在于:
-
<clinit>执行期间,该类的初始化锁被独占;其他线程若此时尝试首次使用该类(如访问非编译期常量的静态字段、调用静态方法、new实例等),就会阻塞等待锁释放 - 若线程 A 在执行
A.<clinit>时调用了B.doSomething(),而 B 尚未初始化,则 JVM 会暂停 A 的初始化,转而去执行B.<clinit> - 若此时线程 B(或另一个线程)也正在执行
B.<clinit>,且其中又调用了A.SOME_VALUE(非编译期常量),就会反过来等待A.<clinit>完成——双方各自持有一把锁、等待对方释放另一把锁,死锁形成
典型代码模式与触发条件
以下结构极易引发此类问题,尤其在多线程启动、工具类解耦不当时:
- A 类静态块中创建 B 的实例或调用 B 的静态方法,而 B 类尚未初始化
- B 类静态块中反向引用 A 的非编译期常量(例如
public static final String TOKEN = System.getenv("TOKEN");),导致访问即触发 A 初始化 - 两个类通过静态字段间接依赖:A 中
static final Config CONF = new Config(B.URL);,B 中static final String URL = A.BASE + "/api";,且A.BASE不是编译期常量(如来自Properties加载) - 使用
Class.forName("A")和Class.forName("B")分别在不同线程中触发,而非显式控制初始化顺序
如何快速定位问题类
不要靠日志猜,直接抓线程快照分析:
- 执行
jstack -l <pid>获取完整线程 dump - 搜索关键词:
java.lang.Class.forName、at <clinit>、BLOCKED (on object monitor) - 找到至少两个线程,堆栈中分别出现
A.<clinit>等待B.class,以及B.<clinit>等待A.class - 注意:若看到
at java.lang.ClassLoader.loadClass后紧跟at <clinit>,说明初始化正卡在链路中间,不是类找不到,而是锁没拿到
实用规避与修复策略
核心原则是打破初始化依赖环,而非简单加锁或延迟:
- 将相互依赖的静态逻辑下沉为懒加载方法(如
private static volatile Config config;+ 双重检查同步块),避免在<clinit>中执行 - 确认哪些静态字段属于“编译期常量”(
public static final String X = "abc";),它们会被内联,不触发初始化;非编译期常量(含构造函数调用、系统属性读取等)务必审慎使用 - 统一初始化入口:用单例 Holder 模式或 Spring
@PostConstruct控制顺序,让 A 和 B 的初始化由同一个协调者按需触发 - 测试阶段加入初始化探测:在单元测试启动时,主动调用
Class.forName("A"); Class.forName("B");并捕获LinkageError或超时,提前暴露风险

















