Java 序列化方案对比与性能

系统对比 Java 序列化方案:JDK 原生序列化的原理与安全风险、Jackson 与 Gson 等 JSON 方案、Kryo 高性能二进制、Protobuf 结构化方案,给出性能基准对比、适用场景与选型指南。

序列化决定数据的体积、传输的速度与协议的兼容性,也往往是性能瓶颈和安全隐患的双重温床。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 对比

维度JacksonGson
性能较快较慢
功能注解丰富、流式 API简单易用
泛型TypeReferenceTypeToken
生态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 APIJackson可读、跨语言、生态成熟
微服务 RPCProtobuf + gRPC跨语言、小体积、强类型
Java 内部 RPCKryo快、小、无需跨语言
Redis 缓存值Kryo/Protobuf体积小省内存
消息队列Protobuf/Avroschema 演进好
遗留系统互操作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」更多文章

  1. Java 并发设计模式实战
  2. OOM 排查与堆转储分析
  3. Arthas 线上诊断实战