java类加载死锁非标准线程死锁,而是多线程并发初始化时因静态依赖闭环、自定义加载器委派失当或资源顺序混乱,导致线程卡在loadclass/forname中呈runnable无进展;需用jstack -l抓取循环调用及锁地址循环引用,并剥离static{}中io、网络等非原子操作,规范自定义加载器仅重写findclass且显式委托父加载器。

Java 类加载器死锁不是标准意义上的线程死锁,而是多线程并发触发类初始化(<clinit></clinit>)时,因静态依赖闭环、自定义加载器委派失当或资源加载顺序混乱,导致线程卡在 ClassLoader.loadClass 或 Class.forName 中,状态为 RUNNABLE 却无实际进展。它不抛异常、不占高 CPU,但会阻塞后续所有依赖该类的调用。
看线程栈,抓闭环线索
用 jstack -l <pid></pid> 抓取线程快照,重点找三类信号:
- 堆栈中连续出现多个
<clinit></clinit>调用,例如A.<clinit> → B.<clinit> → A.<clinit></clinit></clinit></clinit> - 成对出现
waiting to lock和locked,且锁地址构成循环引用(如 A 等 B、B 等 C、C 又等 A) - 阻塞点落在
Class.forName、ClassLoader.loadClass或静态字段首次访问处 - 避免在
<clinit></clinit>里打日志——SLF4J、Log4j 的LoggerFactory本身可能正卡在初始化竞争中;改用System.err.println输出明确断点
砍掉静态初始化里的“雷区”
静态块不是“加了 try-catch 就安全”,真正危险的是:异常后仍继续访问未就绪类。必须剥离所有非原子、非确定性操作:
- 禁止在
static{}中读取配置文件、环境变量、系统属性(IO 不稳定) - 禁止发起网络请求、连接数据库、调用远程服务
- 禁止解析 JSON/XML、反序列化外部数据
- 禁止直接调用其他类的
static方法(尤其该方法内部含 new、IO 或依赖注入) - 改用 Holder 模式延迟初始化:
private static class Holder { static final Service INSTANCE = new Service(); },首次访问Holder.INSTANCE才触发 - 或封装为显式
init()方法,由调用方控制重试、降级与告警
规范自定义类加载器行为
90% 的“类加载锁死锁”源于加载器破坏双亲委派或锁管理失控:
- 构造时必须显式传入父加载器:
super(ClassLoader.getSystemClassLoader());否则父为null,连java.lang.Object都找不到 - 只重写
findClass(String),不要重写loadClass(String)——绕过双亲委派会引发LinkageError或ClassCastException - 在
findClass中先调用findLoadedClass(name)避免重复 define,再读字节码,最后调用defineClass(name, bytes, 0, bytes.length) - 资源加载统一用
this.getClass().getClassLoader().getResource("xxx"),不硬编码路径,防止因加载器不同导致资源不可见
应急预热 + 根治重构
历史系统来不及大改时,可快速缓解:
- 用
-XX:+TraceClassLoading观察哪些类反复出现在多条加载链路中(如protobuf.GeneratedMessageV3、slf4j.LoggerFactory) - 在应用启动早期,用系统类加载器显式加载一次冲突类:
ClassLoader.getSystemClassLoader().loadClass("xxx.YourConflictClass") - 若使用 Spring,可在
ContextRefreshedEvent中对含静态块的类做预热:Class.forName(name, false, ClassLoader.getSystemClassLoader()) - 根治方案是三步走:只重写
findClass、显式委托父加载器加载历史包、解耦静态初始化逻辑
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











