java原生序列化不支持循环引用,因采用无状态深度优先遍历且不记录已访问对象,遇a↔b闭环即栈溢出;推荐改用xstream、kryo、fst或jackson等支持对象图语义的库。

Java 原生序列化(ObjectOutputStream)不支持循环引用,遇到对象 A 引用 B、B 又引用 A 的情况,会无限递归遍历,最终抛出 StackOverflowError。这不是 bug,而是设计使然——它不做图遍历,只做无状态的深度优先遍历,且不记录已访问对象。
原生序列化为何失败
Java 默认序列化机制把对象看作一棵树,而非有向图。一旦出现闭环(如父子双向引用、链表成环、树节点回指父节点),writeObject 就会反复进入同一对象,栈空间迅速耗尽。
- 所有参与序列化的类必须实现
Serializable,否则直接报NotSerializableException -
transient字段会被跳过,但不会在反序列化后自动初始化(仍是null或默认值) - 无法通过简单配置开启“引用跟踪”,必须换方案或手动干预
推荐的实用解决方式
不建议重写 writeObject/readObject 来手动维护引用表——开发成本高、易出错、难以维护。更主流的做法是切换到支持对象图(Object Graph)语义的序列化库:
-
XStream:默认启用引用解析(
allowTypes+ 自动 ID 分配),无需额外配置即可处理循环引用 -
Kryo:需显式调用
setReferences(true),性能高,适合 RPC 或游戏服务等对吞吐敏感场景 - FST:零配置支持循环引用,序列化体积小、速度快,兼容 JDK 版本较广
-
Jackson:通过注解控制,如
@JsonManagedReference/@JsonBackReference拆分双向关系;或全局配置SerializationFeature.FAIL_ON_SELF_REFERENCES关闭校验并配合@JsonIdentityInfo实现 ID 引用
Jackson 和 Gson 的典型配置
如果项目已绑定 Jackson,可按字段粒度处理:
- 用
@JsonIgnore直接忽略某一边(例如 User 类中orders字段),适合前端不需要反向数据的场景 - 用
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")让 Jackson 为每个对象生成唯一标识,重复引用处输出"$ref": "1"形式 - Gson 需引入
gson-extras,使用GraphAdapterBuilder注册类型适配器,内部基于 ID 映射实现闭环安全序列化
设计层面的预防建议
技术方案治标,结构优化治本。日常开发中可主动降低循环依赖风险:
- DTO 层与领域模型分离,传输对象只保留单向引用(如返回 User 时不带 orders 列表,改用 orderIds 数组)
- 对必须双向关联的场景,用
WeakReference或SoftReference包装反向字段(注意:仅适用于非序列化场景) - 数据库映射时,用
@Transient标记运行时计算字段,避免 JPA 实体间强耦合引发的序列化连锁反应
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











