核心问题是反序列化时对象重建过重且无法回收。java原生序列化会隐式构建完整对象图,导致堆内存与元空间暴涨oom;应禁用默认类解析、预检流长、限嵌套深度、清理污染字段,并以protobuf/flatbuffers/json+gzip替代。
核心问题不在“序列化得多”,而在“反序列化时建得重、收不回”。老版本 java 原生序列化(objectoutputstream/objectinputstream)在反序列化阶段会隐式重建完整对象图,触发大量中间对象实例化、反射类加载、代理类生成,极易引发堆内存暴涨、元空间持续增长,最终 oom。
识别是否是序列化引发的对象膨胀
出现以下任一现象,高度提示原生序列化是罪魁祸首:
- 堆直方图(
jmap -histo)中java.util.HashMap$Node、sun.reflect.annotation.AnnotationInvocationHandler、java.lang.Class实例数量异常激增,且调用栈可追溯至ObjectInputStream.readObject() - 单次反序列化后,对象图深度 >10 层、子引用数 >100,但业务逻辑根本不需要如此复杂的结构
-
jstat -gc显示MU(Metaspace Used)持续缓慢上涨,且ClassLoader未被卸载(如存在LeakClassLoader) - 传输一个仅几百 KB 的字节流,反序列化后堆中却出现数 MB 的
byte[]或Object[]数组
立即阻断反序列化阶段的隐式扩张
不依赖默认行为,从入口处收紧控制:
- 禁用默认类解析:继承
ObjectInputStream,重写resolveClass,只允许白名单内的类(如com.myapp.dto.开头)被加载 - 预检字节流:在调用
readObject()前,检查输入流总长度,单次建议 ≤512 KB;超限直接拒绝 - 限制嵌套深度:自定义
ObjectInputStream,重写readObjectOverride,每进入一层对象递归就计数,超过 5 层立即抛InvalidClassException - 清理污染字段:严禁将
ThreadLocal、ClassLoader、Connection、含静态缓存的工具类等上下文对象放入可序列化字段——它们会拖入不可见的强引用链,阻碍 GC
用轻量协议替代原生序列化
真正的大变量(报表数据、批量日志、图像元信息)必须换协议:
- 结构化数据优先选 Protobuf:体积约为 Java 原生序列化的 1/4,无反射、无任意代码执行,需提前定义
.protoschema;或选 FlatBuffers:支持零拷贝读取,适合只读高频场景 - 纯文本大数据走 JSON + GZIP:用 Jackson 配置
SmileFactory(二进制 JSON)提升效率;对浮点数启用WRITE_NUMBERS_AS_STRINGS避免精度丢失;压缩后传输,实测压缩率常达 60%+ - 超大二进制(如文件切片)改用分块传输:每次只传 ≤1 MB 的
byte[],服务端按序拼接并用 MD5 校验完整性,避免单次加载巨型数组
辅助加固与验证手段
光换协议不够,还需配套动作确保落地有效:
- 在 RPC 框架或消息中间件层统一拦截:所有入参/出参强制走新协议,禁止
Serializable类型跨服务流动 - 上线前做反序列化压力测试:构造深度/宽度边界值的 payload,监控堆内存、元空间、GC 频次变化
- 开启 JVM 参数
-XX:+TraceClassLoading和-XX:+TraceClassUnloading,确认无非预期类被反复加载 - 对存量 DTO 类加注解标记(如
@DeprecatedSerialization),配合 SonarQube 规则扫描,杜绝新代码误用
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











