序列化决定数据的体积、传输的速度与协议的兼容性,也往往是性能瓶颈和安全隐患的双重温床。JDK 原生序列化虽然方便,但体积大、有反序列化漏洞;JSON 生态普及但偏慢;Kryo、Protobuf 在性能与体积上各有优势。本文从原理讲到基准,帮你为 RPC、缓存、消息队列选对方案。
一、序列化方案全景
1.1 核心指标
| 指标 | 说明 |
|---|---|
| 体积 | 序列化后字节数,影响带宽与存储 |
| 速度 | 序列化/反序列化吞吐,影响 QPS |
| 兼容性 | 字段增删是否破坏旧数据 |
| 语言支持 | 是否支持跨语言 |
| 安全性 | 反序列化是否存在漏洞风险 |
1.2 主流方案一览
四大阵营:
JDK 原生 —— 方便但慢、大、有漏洞
JSON —— 可读、跨语言、中等性能(Jackson/Gson)
二进制通用 —— 快、小(Kryo/FST),但需注册类
结构化协议 —— 跨语言、强类型(Protobuf/Thrift/Avro)
一句话总结: 选序列化方案就是在体积、速度、兼容、安全、跨语言五个指标间做取舍;没有通吃方案,只有「这个场景最合适」。
二、JDK 原生序列化
2.1 原理与实现
JDK 原生序列化用 ObjectOutputStream/ObjectInputStream,基于反射把对象图写入流,包含类名、字段、结构信息,因此体积大。
// 必须实现 Serializable
@Serial
private static final long serialVersionUID = 1L; // 建议显式声明
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User u = (User) ois.readObject();
}
2.2 为什么不建议生产使用
| 问题 | 说明 |
|---|---|
| 体积大 | 带类元信息,比二进制大 3~10 倍 |
| 性能差 | 反射 + 大量对象分配 |
| 兼容脆弱 | serialVersionUID 不匹配即失败 |
| 安全风险 | 反序列化漏洞(攻击者构造恶意流) |
| 不可读 | 二进制不可调试 |
安全结论(Oracle 官方立场):
不建议在 Java 9+ 继续使用 Java 序列化
可用 ObjectInputFilter 做类白名单,但仅是缓解
新系统一律选用 JSON / Kryo / Protobuf
一句话总结: JDK 原生序列化「能用但别用」——体积大、性能差、安全风险高;除了遗留系统互操作,新代码应直接避开。
三、JSON 系列:Jackson 与 Gson
3.1 Jackson 基础用法
// Jackson:性能领先、功能全
ObjectMapper mapper = new ObjectMapper();
// 序列化
String json = mapper.writeValueAsString(user);
// 反序列化
User user = mapper.readValue(json, User.class);
// 泛型反序列化:用 TypeReference 保留泛型信息
List<User> users = mapper.readValue(json,
new TypeReference<List<User>>() {});
3.2 Gson 与 Jackson 对比
| 维度 | Jackson | Gson |
|---|---|---|
| 性能 | 较快 | 较慢 |
| 功能 | 注解丰富、流式 API | 简单易用 |
| 泛型 | TypeReference | TypeToken |
| 生态 | Spring 默认 | Android 常用 |
// Gson 泛型反序列化
Gson gson = new Gson();
List<User> users = gson.fromJson(json, new TypeToken<List<User>>() {}.getType());
3.3 性能优化技巧
// 1. 复用 ObjectMapper(线程安全,可共享)
// 2. 用流式 API 避免中间字符串
// 3. 关闭不必要的特性
ObjectMapper mapper = new ObjectMapper()
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
JSON 适用场景:
需要人类可读、跨语言(JS/Go 等)互操作
HTTP API、配置文件、日志
RPC/缓存需要极致性能时,JSON 通常不够快
一句话总结: JSON 是「可读性与跨语言」的通用选择,Jackson 性能更优、Gson 更简单;作为默认方案很稳,但追求极致性能时会被二进制方案拉开差距。
四、Kryo:高性能二进制序列化
4.1 基本用法
Kryo 通过类注册(避免写类全名)和对象引用跟踪大幅压缩体积、提升速度。
// 关键:注册类,减少体积
Kryo kryo = new Kryo();
kryo.register(User.class, 101); // 用整数 ID 代替全类名
kryo.setReferences(true); // 对象引用跟踪,省重复
// 序列化
Output out = new Output(1024);
kryo.writeObject(out, user);
byte[] bytes = out.toBytes();
// 反序列化
Input in = new Input(bytes);
User u = kryo.readObject(in, User.class);
4.2 Kryo 的坑
| 坑 | 说明 | 对策 |
|---|---|---|
| 线程不安全 | Kryo 实例非线程安全 | ThreadLocal 或对象池 |
| 类注册漂移 | ID 顺序变则数据读不了 | 固化注册顺序,不要增删乱序 |
| 无 schema 演进 | 字段增删不友好 | 不适合长期演进的数据 |
| 跨语言差 | 仅 Java 生态 | RPC 需跨语言则选 Protobuf |
// ThreadLocal 复用 Kryo,避免并发问题
static final ThreadLocal<Kryo> KRYO = ThreadLocal.withInitial(() -> {
Kryo k = new Kryo();
k.register(User.class, 101);
return k;
});
一句话总结: Kryo 用「类注册 + 引用跟踪」换来体积小、速度快;但线程不安全、注册顺序漂移、无跨语言——适合 Java 内部 RPC 与缓存。
五、Protobuf:跨语言结构化方案
5.1 定义与生成
Protobuf 用 .proto 定义消息,编译生成各语言代码,采用 tag 编码体积极小,天然支持跨语言与版本演进。
// user.proto
syntax = "proto3";
package demo;
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
# 生成 Java 代码
protoc --java_out=./src user.proto
5.2 Java 中使用
// 序列化
UserOuter.User user = UserOuter.User.newBuilder()
.setId(1L).setName("alice").setEmail("a@b.com").build();
byte[] bytes = user.toByteArray();
// 反序列化
UserOuter.User parsed = UserOuter.User.parseFrom(bytes);
5.3 Protobuf 的优势与代价
| 优势 | 代价 |
|---|---|
| 跨语言(Java/Go/Python/C++) | 需要 proto 文件与代码生成 |
| 体积极小(tag 编码) | 引入编译期依赖 |
| 强类型 + 向前向后兼容 | 学习曲线高于 JSON |
| 性能优秀 | 调试需转文本(json_format) |
适用场景:
跨语言 RPC(gRPC 默认)
大数据平台、消息协议
长期演进的内部协议
一句话总结: Protobuf 是「跨语言 + 小体积 + 强类型」的工业标准,gRPC 与微服务通信的首选;代价是代码生成与 proto 管理成本。
六、性能对比与基准
6.1 典型基准数据
一个中等结构(约 20 字段)的对比(相对比例,环境不同有差异):
方案 序列化速度 反序列化速度 体积
JDK 原生 1x 1x 100(基准)
Gson 2x 1.5x 55
Jackson 3x 2.5x 50
Kryo 8x 6x 20
Protobuf 10x 8x 15
结论:二进制方案在速度与体积上全面占优,JSON 居中,JDK 最差。
6.2 实测方法
// 正确的基准姿势:JMH,预热 + 多次迭代
@Benchmark
public void kryoWrite(Blackhole bh) {
Kryo k = kryoTl.get();
Output out = new Output(1024);
k.writeObject(out, user);
bh.consume(out.toBytes());
}
基准注意:
1. 必须预热(JIT 生效)
2. 复用对象,测稳态吞吐
3. 相同数据、相同机器对比
4. 关注反序列化(通常更慢)而非只测序列化
一句话总结: 基准结论稳定——Protobuf/Kryo 在速度与体积上领先,JSON 居中,JDK 原生垫底;测基准用 JMH,不要用随手写的循环。
七、选型指南与安全
7.1 场景选型矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| HTTP JSON API | Jackson | 可读、跨语言、生态成熟 |
| 微服务 RPC | Protobuf + gRPC | 跨语言、小体积、强类型 |
| Java 内部 RPC | Kryo | 快、小、无需跨语言 |
| Redis 缓存值 | Kryo/Protobuf | 体积小省内存 |
| 消息队列 | Protobuf/Avro | schema 演进好 |
| 遗留系统互操作 | JDK 原生(白名单) | 仅能兼容旧数据 |
7.2 安全清单
反序列化安全规范:
1. 不反序列化不可信来源的 JDK 序列化流
2. 如必须用,配置 ObjectInputFilter 类白名单
3. JSON 反序列化注意多态类型(Jackson 默认关闭)
4. 校验输入长度,防超大 payload 攻击
// Jackson 安全实践:显式配置,禁用默认多态
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 或直接禁用多态,用 DTO 做输入校验
一句话总结: 选型跟着场景走——对外 HTTP 用 JSON、跨语言 RPC 用 Protobuf、Java 内部性能敏感用 Kryo;无论选哪个,反序列化都必须做输入校验与安全白名单。
八、实战陷阱清单
| 陷阱 | 现象 | 对策 |
|---|---|---|
| Kryo 未注册类 | 体积暴增、性能下降 | 提前注册所有业务类 |
| Kryo 跨线程用 | 数据错乱 | ThreadLocal 隔离 |
| JDK 序列化漏洞 | 远程代码执行风险 | 弃用或加白名单过滤 |
| JSON 泛型丢失 | List 反序列化成 List | 用 TypeReference/TypeToken |
| Protobuf 字段删改 | 新老版本不兼容 | 保留字段号,只增不删 |
| 大对象序列化 | 内存与延迟飙升 | 分批/压缩/换更优方案 |
| 忽略安全校验 | 被超大 payload 打崩 | 长度限制 + 类型校验 |
九、总结
| 方案 | 速度 | 体积 | 跨语言 | 安全 | 适用 |
|---|---|---|---|---|---|
| JDK 原生 | 低 | 大 | 否 | 低 | 遗留系统 |
| JSON | 中 | 中 | 是 | 中 | HTTP API |
| Kryo | 高 | 小 | 否 | 中 | Java 内部 |
| Protobuf | 高 | 小 | 是 | 高 | 跨语言 RPC |
一句话记住:序列化选型 = 场景 × 指标。对外要可读可调、跨语言选 JSON/Protobuf;对内要极致性能选 Kryo;JDK 原生序列化请默认避开,并始终把反序列化安全放在第一位。数据格式是协议的一部分,定了就很难改,值得多花时间对比。
延伸阅读
- Java 泛型与反射深度实战 — TypeReference/TypeToken 依赖的 Type 体系
- Java 性能优化:从代码到 JVM 的全链路调优 — 序列化在性能链路中的定位
- Spring Boot 深度解析:自动配置与 Starter 原理 — Jackson 在 Spring 中的自动配置与定制
- Spring Cloud 微服务实战 — 微服务通信中的序列化协议选型
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。