classcircularityerror是jvm在类加载链接阶段检测到类定义存在直接或间接自引用时主动抛出的linkageerror,不抛stackoverflowerror因其发生在字节码执行前、无递归调用栈。

ClassCircularityError 是什么,为什么它不抛 StackOverflowError?
java.lang.ClassCircularityError 不是运行时方法调用栈溢出,而是类加载器在「链接阶段」(尤其是初始化前的准备/解析)检测到类定义存在**直接或间接自引用**时,主动中止加载并抛出的 LinkageError 子类。它发生在 JVM 尚未执行任何 Java 字节码之前,所以你不会看到堆栈里有一长串递归调用 —— 它压根没开始跑你的 static 块。
常见触发场景包括:
- 类 A 的
static字段初始化直接调用类 B 的静态方法,而类 B 的static初始化又反向依赖类 A 的字段或方法 - 两个类通过
static final常量互相引用对方的编译期常量(看似安全,但若其中一方含非常量表达式或触发类初始化,则破防) - 使用了自定义
ClassLoader,且子加载器在父加载器尚未完成初始化时就尝试loadClass("A"),而 A 又触发自身加载逻辑
怎么快速定位哪个类在循环?看日志和字节码就够了
错误信息本身通常只报出最外层出问题的类名,比如:java.lang.ClassCircularityError: com.example.ServiceA。但这只是“被卡住”的出口,不是根源。真正要查的是谁在初始化 ServiceA 时把它拉进了死循环。
实操建议:
- 启用 JVM 类加载日志:
-XX:+TraceClassLoading -XX:+TraceClassResolution,启动时加参数,观察ServiceA被加载前后,哪些类紧接着被 resolve 或 initialize - 用
javap -c -s ServiceA查看其静态初始化块(<clinit></clinit>)里是否调用了ServiceB.getXXX(),再反过来查ServiceB的<clinit></clinit> - 如果用了 Lombok 的
@UtilityClass或@Slf4j,注意它们会悄悄生成static字段 —— 这些字段初始化可能隐式触发其他类加载
重构代码:三招避开 static 初始化陷阱
绝大多数 ClassCircularityError 都能靠改写静态初始化逻辑解决,不用动类加载器。
关键原则:**让类的初始化变成“单向”和“延迟”**。
- 把
static final int VALUE = ClassB.SOME_CONST;改成static final int VALUE = 42;(硬编码)或移到非 static 上下文 - 把
private static final Helper HELPER = new Helper();拆成懒加载:private static volatile Helper helper;+public static Helper getHelper() { ... }(双重检查) - 提取公共静态状态到一个独立、无依赖的配置类(如
Constants),确保它不引用任何业务类;所有业务类只读它,不反向依赖
示例对比:
// ❌ 危险:A → B → A
class A { static int x = B.y; }
class B { static int y = A.x + 1; }
// ✅ 安全:拆开依赖,延迟求值
class A { static int x() { return B.y(); } }
class B { static int y() { return 100; } }
别碰类加载器,除非你真懂 defineClass 的时机
网上有些方案建议“换一个 ClassLoader”或“重写 loadClass”,这在绝大多数 Spring Boot、Tomcat 或 Android 场景下不仅无效,反而引入更难调试的隔离问题。因为 ClassCircularityError 是 JVM 在同一个类加载器内做语义校验时拒绝加载,不是跨加载器可见性问题。
真正需要干预类加载器的情况极少,仅限于:
- 你在用
Unsafe.defineClass动态生成类(比如某些 AOP agent、greys、arthas 插件) - 你实现了自己的 OSGi BundleClassLoader 或模块化热替换机制
- 你正在写 JVM Agent,并在
transform回调里修改了类的字节码,导致新 class 引用旧 class 的未完成版本
普通业务开发遇到这个错,99% 应该回退到代码层 —— 看清哪个 static 块在试图“唤醒还没睡醒的自己”。










