Java类加载器遇到初始化阶段循环依赖时直接抛出ClassCircularityError,因JVM在链接阶段检测到类处于“正在初始化”状态又被重复请求;需从代码结构上消除静态初始化闭环,如拆分静态逻辑、推迟依赖时机、避免静态字段互相引用。

Java 类加载器本身不主动“处理”循环依赖,而是直接拒绝——遇到类初始化阶段的循环依赖时,会抛出 java.lang.ClassCircularityError。这不是运行时逻辑错误,而是 JVM 在链接(linking)阶段检测到非法依赖路径后强制终止加载的结果。
类初始化阶段的循环依赖是怎么触发的
关键在于静态初始化块或静态字段赋值中,**直接或间接引用了尚未完成初始化的自身或相互依赖的类**:
- 类 A 的静态字段初始化调用了类 B 的静态方法,而类 B 的静态初始化又反过来依赖类 A 的静态字段
- 类 A 的
<clinit>方法执行过程中,触发类 B 加载;类 B 的<clinit>又试图访问类 A 的静态成员,此时类 A 还在“正在初始化(Initializing)”状态 - JVM 规范要求:若一个类处于“初始化中”,再次请求其初始化,必须抛出
ClassCircularityError
真正有效的应对方式不是“绕过”,而是切断初始化链
不能靠延迟加载或代理来解决这类错误,因为问题发生在类加载器内部状态层面。必须从代码结构上消除静态初始化环节的闭环:
- 拆分静态逻辑:把跨类的静态依赖,改为运行时按需计算(例如用方法返回值替代静态字段直接赋值)
-
推迟依赖时机:将原本在
static {}中完成的初始化,移到首次调用的普通方法里,或使用Holder 模式延迟加载 -
避免静态字段互相引用:比如
ClassA.VALUE = ClassB.DEFAULT和ClassB.DEFAULT = ClassA.MAX这类双向静态赋值必须打破 -
用常量或字面量替代类间静态引用:如把
ClassB.SOME_VALUE换成具体数字或提取为公共常量接口
Spring 环境下要注意区分两类“循环依赖”
Spring 容器的循环依赖(如 Bean A 注入 Bean B,B 又注入 A)和 JVM 类加载期的 ClassCircularityError 是两回事:
立即学习“Java免费学习笔记(深入)”;
- Spring 的三级缓存机制只解决 Bean 实例化与属性注入阶段 的循环依赖,它无法干预类本身的
<clinit>执行顺序 - 如果两个 Bean 对应的类在静态初始化阶段就已形成循环,Spring 还没开始创建 Bean,JVM 就已报错退出
- 因此,即使加了
@Lazy或改用 setter 注入,也无法挽救类加载阶段的循环依赖
排查和验证建议
当看到 ClassCircularityError,不要查 Spring 配置,而要聚焦在堆栈中的类及其静态初始化代码:
- 查看异常堆栈最顶层的类名,定位它的
static {}块或静态字段初始化语句 - 顺藤摸瓜检查被调用的其他类是否也含静态初始化,是否存在双向静态引用
- 用
javap -c 类名查看<clinit>字节码,确认初始化流程中的调用链 - 临时注释部分静态逻辑,验证是否能成功加载——这是最直接的定位手段


















