Skip to content

JDK 序列化问题排查 ​

  • 标签:后端、线上问题排查

🏛 业务架构师 ​

JDK 原生序列化的问题排查和替换,我按"现象 → 排查 → 结论 → 选型"讲。

典型故障现象

  • 跨服务调用报 InvalidClassException(serialVersionUID 不一致——一边改了类没改版本号)
  • 反序列化直接失败 ClassNotFoundException(消费方没有对应类)
  • RPC 响应体异常巨大、耗时高
  • 安全告警:反序列化漏洞(CVE 链,如 Commons-Collections gadget)被扫出

排查手段

  • 开启 -Dsun.io.serialization.extendedDebugInfo=true 拿到序列化对象路径细节
  • 抓包/落盘序列化字节流,用工具分析对象构成,通常是某个大集合或嵌套对象把体积撑爆
  • 核对双方类的字段和 serialVersionUID 定义

JDK 序列化的原罪(为什么必须换)

  1. 体积大:携带类元信息,同样数据比 JSON 大 3~5 倍,比 Protobuf 大 5~10 倍——带宽和存储的真金白银
  2. 性能差:反射密集,大对象序列化秒级
  3. 跨语言不通:Java only,和任何非 JVM 系统对接就死
  4. 不安全:反序列化漏洞是 RCE 重灾区,攻击者构造恶意字节流直接执行代码——这已经是安全合规问题,不只是技术选型

替换选型(按场景)

  • 对外 API、可读性要求:JSON(Jackson)
  • 内部高性能 RPC:Protobuf / Kryo(体积小一个数量级)
  • 缓存对象:JSON 或 Protobuf,配合版本兼容策略(新增字段可选、老消费方跳过未知字段)

收尾:我治理存量系统的原则是新代码一律禁用 JDK 序列化(代码规约 + 静态扫描卡点),存量按故障驱动逐步替换——全面重写的历史包袱改造,投入产出通常不划算,除非它已经造成过资损或正在被安全审计追着跑。

🏗 系统架构师 ​

JDK 序列化的问题我按"踩坑点 → 排查 → 替代"来讲。

典型故障现象:RPC/Dubbo 老版本调用报 java.io.InvalidClassException(local class incompatible, serialVersionUID 不同)、序列化后体积暴涨、CPU 被序列化吃满、反序列化失败导致服务间调用大面积报错。

排查路径:

  1. 看异常栈确定是序列化还是反序列化阶段,serialVersionUID 不一致是最高频原因——类加了字段但两端版本不同步
  2. 检查被序列化对象是否实现了 Serializable,嵌套的子对象漏实现会运行时才炸
  3. 静态和 transient 字段不参与序列化,排查"字段莫名为 null"时先想到它
  4. 抓包或看 MQ 消息体大小,JDK 序列化带大量类元数据,同样的 POJO 体积是 JSON 的 3~5 倍、Protobuf 的 5~10 倍

JDK 序列化的固有缺陷(这也是面试官想听的):

  • 体积大、性能差(反射 + 全量类元数据)
  • 跨语言不可用,只限 Java
  • 安全问题:反序列化漏洞(利用 gadget chain RCE),这也是官方逐步废弃它的原因
  • 无法向前兼容,类演进就炸

治理方案:新链路一律换 JSON(调试友好)或 Protobuf/Kryo/Hessian2(性能敏感,Dubbo 默认 hessian2);存量系统过渡期显式声明 serialVersionUID 锁定版本,只增字段不删不改类型;对外暴露的接口绝不接受 JDK 序列化输入,防反序列化攻击。

💻 全栈开发专家 ​

JDK 序列化线上故障我处理过的典型三类,最后都换掉了 JDK 序列化。

三类问题

  1. serialVersionUID 不一致:两端类版本不同(加了个字段但没固定 UID),反序列化直接抛 InvalidClassException。排查:报错栈里有明确的 localVersionUID 与 stream serialVersionUID,serialver 命令可算。
  2. 类结构不兼容:即使 UID 固定,字段类型变了/删了字段,读出的对象缺字段或抛 StreamCorruptedException,问题隐藏在二次使用时才爆。
  3. 体积大、性能差:JDK 序列化带大量类元数据,比 JSON/Protobuf 大 3~5 倍、慢一个量级,Redis 里存这种 value 直接拖垮网络和 GC。

排查手段

  • 二进制不可读,先 xxd 看文件头(JDK 序列化魔数 ACED0005),确认真的是 JDK 格式而不是被中间件二次包装。
  • 开 -Djava.io.serialization.debug 或在 readObject 处埋点打印类名和 UID。
  • 跨服务版本不一致时,把双方 jar 包的类拉下来 serialver 比对。

根治:换序列化协议

yaml
# Dubbo 换 hessian2(默认)/fastjson2
dubbo.protocol.serialization: hessian2

缓存对象统一 JSON(加类型字段)或 Protobuf;跨语言、演进频繁的场景 Protobuf 的字段编号兼容性远好于 JDK 序列化。还有一点安全:JDK 反序列化有 gadget 链 RCE 历史(Commons-Collections),攻击面本身就是换掉它的理由。