这类死锁是jvm类初始化锁与阻塞式网络i/o交织导致的静态初始化永久阻塞,表现为启动卡住、线程waiting在java.lang.class上,堆栈含和socket调用;排查需查static块、static final字段及@postconstruct中网络操作,修复应推迟初始化、设超时或异步加载。

这类死锁不是传统意义上的“两个线程互相持有对方需要的锁”,而是 JVM 类加载机制与阻塞式网络 I/O 交织引发的**类初始化锁等待 + 线程永久阻塞**,最终表现为系统卡在启动阶段、线程数停滞、无明显异常日志。核心在于:静态初始化块中发起未设超时的长连接(如 HTTP 客户端初始化、ZooKeeper 连接、Redis 同步连接),导致该类永远无法完成 `
识别是否真卡在类初始化 + 网络阻塞
先确认现象是否匹配:
- 应用启动后长时间无响应,jstack 显示多个线程状态为 WAITING on java.lang.Class(注意不是普通对象锁),且堆栈顶部含
java.lang.Class.forName0或ClassLoader.loadClass - 某个线程堆栈停留在网络调用处,例如:
at sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:682)或at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(...),且上方紧邻com.xxx.ConfigLoader.<clinit>(ConfigLoader.java:23)</clinit> - 没有“Found one Java-level deadlock”提示,但大量线程处于 WAITING/BLOCKED,且锁对象是
java.lang.Class@xxx地址
定位触发类和初始化链
重点查三类代码位置:
- 所有 static {} 块:逐个检查是否直接或间接调用了网络客户端构建、同步连接建立、远程配置拉取等操作
-
static final 字段初始化表达式:例如
private static final HttpClient CLIENT = HttpClientBuilder.create().build();—— 这种看似安全的写法,若内部触发 DNS 解析或连接握手,仍会阻塞整个类初始化 -
@PostConstruct 或 static 工具类中的 init() 方法:有些框架(如老版本 Dubbo)会在静态上下文中触发远程注册,需结合字节码反编译(
javap -c 类名)看是否隐含Class.forName或静态字段读取
验证与临时绕过
不改代码也能快速验证猜想:
- 加 JVM 参数启动:
-XX:+TraceClassLoading -XX:+TraceClassUnloading,观察日志中是否某类“开始加载”后长期无“加载完成”记录 - 用
strace -p <pid> -e trace=connect,sendto,recvfrom</pid>查看进程是否卡在某个 socket 系统调用上 - 临时在可疑 static 块开头加
System.err.println("About to connect in " + getClass().getSimpleName());,看日志是否输出后中断 - 设置全局网络超时(仅测试):
-Dsun.net.client.defaultConnectTimeout=3000 -Dsun.net.client.defaultReadTimeout=3000
修复原则:推迟、异步、降级
根本解法是让网络操作脱离类加载生命周期:
- 改用 Holder 模式延迟初始化:将耗时对象包裹在静态内部类中,首次调用 getter 时才触发初始化
-
启动后异步加载:Spring 中用
@EventListener(ContextRefreshedEvent.class)替代 static 块;非 Spring 环境可用Executors.newSingleThreadScheduledExecutor().schedule(...) - 强制设超时并捕获失败:即使必须同步初始化,也要为连接、读取设置毫秒级 timeout,并在 catch 中提供默认值或抛出明确错误,避免“静默卡住”
- 拆分初始化职责:把“加载配置”和“建立连接”分开——前者可静态完成(如读本地 properties),后者交由运行时按需触发










