Java原生序列化不会因循环引用导致栈溢出,因其内置句柄表机制自动识别重复对象并写入TC_REFERENCE标记,而非重复序列化;问题多源于JSON库默认无引用跟踪或手动深拷贝未维护访问缓存。

Java 序列化本身不会因为循环引用导致栈溢出,关键在于用对机制、避开误区。
Java 原生序列化(java.io.Serializable)从设计上就内置了循环引用防护能力。它靠的是内部的对象句柄表(handle table),不是靠“递归深度限制”或“手动跳过字段”,而是自动识别重复对象并替换为引用标记。
Java 原生序列化如何安全处理循环引用
- 序列化时,每写入一个新对象,JVM 就在句柄表中登记它的唯一 ID
- 再次遇到同一对象(哪怕是在 A→B→A 这种回边里),不重复序列化字段,只写入
TC_REFERENCE标记 + 之前分配的 ID - 反序列化时,句柄表同步重建,能准确还原原始引用关系,包括循环结构
- 这个过程对开发者完全透明:只要类实现
Serializable,无需注解、无需配置、无需改字段
比如:
class Node implements Serializable {
String name;
Node parent;
}
Node a = new Node(); a.name = "A";
Node b = new Node(); b.name = "B";
a.parent = b; b.parent = a; // 形成循环
new ObjectOutputStream(out).writeObject(a); // ✅ 正常完成,无栈溢出为什么你可能 still 遇到 StackOverflowError?
真正出问题的,往往不是 ObjectOutputStream,而是以下场景:
立即学习“Java免费学习笔记(深入)”;
- 使用 Jackson / Fastjson / Gson 等 JSON 库做序列化,默认把对象当树遍历,不启用引用跟踪 → 深度递归 → 栈溢出
- 手动实现深拷贝或 toString 逻辑时,没维护已访问对象集合,直接递归访问字段 → 无限调用
- 在责任链、表达式引擎(如 JEXL)、模板渲染等非序列化场景中,自行遍历对象图但未设访问标记
对应解法不是“禁止循环”,而是让框架识别它:
- Jackson:加
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)或全局开启objectMapper.enableDefaultTyping() - Fastjson:1.2.83+ 默认支持
$ref;旧版本设SerializerFeature.DisableCircularReferenceDetect=false - Gson:需配合
GsonBuilder.setExclusionStrategies()或自定义TypeAdapter跳过可疑字段 - toString 安全输出:用
ReflectionToStringBuilder.setCycleDetectionEnabled(true),或用 Lombok 的@ToString(exclude = "parent")
深拷贝时怎么避免循环引发的栈溢出
别依赖 clone() 或简单递归复制——它们不维护对象映射。可靠做法是:
- 复用原生序列化机制:用
SerializationUtils.clone()(Apache Commons Lang),它底层走ObjectInputStream/ObjectOutputStream,复用句柄表逻辑 - 或用广度优先(BFS)方式遍历对象图:用
IdentityHashMap<Object, Object>缓存原始对象与副本的映射,每次遇到已克隆对象,直接链接副本,不新建 - 不要删字段、不要设 transient 来“绕开”循环——这破坏语义,且治标不治本
不复杂但容易忽略


















