
文章收录专栏:Java 核心原理全解:源码・并发・面试实战
前言
本文为 Java 序列化系列 · 下篇(进阶生产篇) 承接上篇基础内容,专注底层原理、对象缓存机制、版本兼容高阶API、序列化代理模式、反序列化安全漏洞、JDK版本变化、数据库存储实战、线上踩坑复盘、企业选型最佳实践。
适合已经掌握Serializable基础,想要深挖底层、规避线上BUG、应对高阶面试的开发者。
👉 基础概念、transient、serialVersionUID、继承规则请阅读上篇: 【面试必背】Java序列化:Serializable、transient、serialVersionUID完整梳理
一、底层核心:对象引用缓存与循环引用
1.1 缓存实现原理
ObjectOutputStream 内部持有一个对象‑输出句柄映射表。 每序列化一个对象,就把对象存入缓存,后续流中遇到同一个对象,不会重复序列化对象数据,只会输出一个引用编号。
带来两个特性:
1.2 高频生产BUG:修改对象后再次序列化不生效
User user = new User("张三",18);
oos.writeObject(user);
// 修改对象属性
user.setName("李四");
oos.writeObject(user);
反序列化出来,第二个对象名字依旧是张三。
原因:流缓存已经记录该对象,直接输出引用,不会重新扫描对象最新状态。
1.3 两种解决方案
面试考点:区分 writeObject 和 writeUnshared 的区别。
二、高阶版本兼容API
2.1 readObjectNoData()
当反序列化时,流里面完全没有当前子类的任何字段数据时,才会回调这个方法。 典型场景:父类子类继承关系发生改动,旧版本字节流缺少子类信息。
注意:仅仅新增几个字段,不会触发 readObjectNoData,很多面试题在这里挖坑。 在方法内可以给字段设置业务默认值,避免字段为null。
2.2 ObjectInputFilter 输入过滤
JDK9 正式引入,JDK8u121之后回移植到JDK8。 用来做反序列化黑白名单,限制允许反序列化的类,拦截恶意 Gadget 类,是JDK官方提供的防御手段。
三、readResolve / writeReplace 深度剖析(面试高频)
上篇简单介绍,这里补充生产坑。
- writeReplace:序列化发生之前,替换要写入流的对象。
- readResolve:反序列化完成,对象构造出来之后,替换最终返回给程序的对象。
3.1 单例序列化漏洞
仅仅单例私有构造,没有 readResolve,反序列化会产生全新对象,破坏单例。
private Object readResolve() {
return INSTANCE;
}
坑点:readResolve 只改变返回对象,流里面依旧保存着旧实例的完整字节数据,不会改变流内容。
3.2 和序列化代理模式的区别
readResolve 只是替换返回对象;序列化代理模式会把代理对象真正写入字节流,安全性更高。
四、Externalizable 生产坑点回顾
Externalizable 完全自己实现读写逻辑。
五、序列化代理模式(Effective Java推荐最安全方案)
普通 readResolve 存在攻击绕过风险,Effective Java 重点推荐序列化代理模式。
核心思路:
// 业务实体
public class User implements Serializable {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
// 序列化代理,写入流的实际是这个代理对象
private static class SerializationProxy implements Serializable {
private final String name;
private final Integer age;
public SerializationProxy(User user) {
this.name = user.name;
this.age = user.age;
}
// 反序列化代理,重建原始对象
private Object readResolve() {
return new User(name, age);
}
}
// 序列化时替换为代理
private Object writeReplace() {
return new SerializationProxy(this);
}
// 禁止直接反序列化本类,防止攻击者构造恶意字节
private void readObject(ObjectInputStream in) throws InvalidObjectException {
throw new InvalidObjectException("请使用序列化代理");
}
}
优势:
- 攻击者无法构造恶意子类进行攻击;
- 反序列化强制调用业务构造器,可以做参数校验;
- 可以无视原对象所有字段访问权限。
缺点:会额外多一层对象,对超大对象会带来少量性能开销。
六、容器类自定义序列化原理:ArrayList / HashMap
JDK集合类并没有简单把全部成员变量序列化。 以ArrayList举例:
- 底层elementData数组会预留大量空位置,如果直接序列化会把大量null空间写入字节流,体积膨胀。
- 重写 writeObject,只序列化数组中有效size个元素;
- readObject读取时,再重建elementData数组。
HashMap同理,会重新构建哈希表,不会直接序列化table数组。
面试考点:为什么集合类要重写writeObject/readObject?减少序列化字节体积。
七、反序列化高危安全漏洞
7.1 漏洞原理
Java原生反序列化不需要调用业务构造器,读取字节即可还原对象。 攻击者构造恶意序列化字节,程序反序列化时,会自动执行类内部readObject等逻辑,配合第三方库的恶意调用链(Gadget),实现远程代码执行。
受影响:JDK原生序列化,只要反序列化不受信任的字节流就有风险。 历史中招组件:Commons‑Collections、Commons‑Beanutils 等。
7.2 分层防御策略
八、其他容易被忽略的序列化坑
九、JDK版本演进:原生序列化现状
- JDK8:序列化完整可用,没有标记废弃;ObjectInputFilter需要升级小版本。
- JDK9:原生序列化被标记为 Legacy(遗留机制),官方明确不推荐继续使用。
- Java17/21:提供开关可以禁用原生序列化;未来版本会逐步移除。
重点:面试经常问,Java官方对原生序列化的态度。
十、数据库存储场景序列化实战规范
很多项目会直接把 Java 序列化后的二进制字节存入数据库BLOB字段,这里有大量线上踩坑点。
10.1 重要概念区分
数据库事务隔离级别 SERIALIZABLE 和 Java 对象序列化完全无关,不要混淆。
10.2 几种存储方案对比
10.3 什么场景可以把对象序列化存入数据库
适合:对象整体保存、整体读取,几乎不会做where条件查询、不需要join关联的数据。 例如:流程快照、历史备份快照、缓存备份。
10.4 什么场景绝对不要序列化存储
需要按对象内部字段做查询、过滤、统计、关联查询,必须拆成数据库普通表字段,不要塞二进制大字段。
10.5 历史遗留系统注意事项
老项目已经使用Java序列化BLOB存入数据库:
十一、生产环境完整最佳实践总结
- 手写固定 serialVersionUID,不要依赖自动生成。
- 敏感字段标记transient,业务层做加密,不要依靠序列化做安全。
写在最后
Java原生序列化看着API简单,但是缓存机制、继承、安全漏洞坑点极多。面试是高频考点,但是新项目尽量避免直接使用。
本系列上篇讲解基础考点,本篇讲解底层原理与生产踩坑,两篇结合完整覆盖Java序列化全部核心内容。
上一篇:【面试必背】Java序列化:Serializable、transient、serialVersionUID完整梳理

标签:Java,序列化,反序列化漏洞,readResolve,writeReplace,生产踩坑,Java安全,JDK,后端面试
网硕互联帮助中心






评论前必须登录!
注册