一、什么是序列化和反序列化?

在深入探讨之前,我们先明确两个核心概念:

序列化(Serialization):将内存中的 Java 对象状态转换为可以存储或传输的字节流格式的过程。简单说,就是把 "活" 的对象变成 "死" 的字节数组。

反序列化(Deserialization):将序列化生成的字节流重新恢复为内存中 Java 对象的过程。也就是把 "死" 的字节数组重新 "复活" 为 "活" 的对象。

Java对象(内存中) → 序列化 → 字节流 → 存储/传输 → 反序列化 → Java对象(内存中)

这一 "对象→字节→对象" 的转换,打破了 Java 对象的时空限制,让对象能够脱离当前 JVM 进程,实现持久化存储和跨进程 / 跨网络传输。


二、为什么需要序列化?

2.1 本质原因:内存的两大局限性

Java 对象之所以需要序列化,本质上是因为内存存在两个天然限制

限制一:内存是易失的(Volatile)
  • JVM 堆内存中的对象随虚拟机启停而存在

  • 程序关闭或服务器断电,内存数据立即消失

  • 对象无法 "存活" 超过程序的生命周期

限制二:内存是隔离的(Isolated)
  • 每个 JVM 进程拥有独立的内存空间

  • 进程 A 无法直接访问进程 B 的内存

  • 不同服务器之间更无法直接共享内存对象

序列化就是为了突破这两个限制而生!


2.2 应用场景示例

场景一:对象持久化存储(跨越时间)

问题:如何让用户的配置信息、游戏存档、会话状态在程序重启后还能恢复?

解决方案:序列化

  • 将用户偏好设置对象序列化后保存到本地文件

  • 将游戏角色状态序列化后存入数据库

  • 将 Session 对象序列化后持久化到磁盘

没有序列化的后果:每次重启程序,所有对象状态都要重新创建,用户体验极差。

场景二:网络传输对象(跨越空间)

问题:分布式系统中,服务 A 如何把一个 Order 对象传给服务 B?

解决方案:序列化

  • 在 Dubbo、Spring Cloud 等 RPC 框架中,方法参数和返回值都需要序列化

  • HTTP 接口中,Java 对象转 JSON 也是一种序列化形式

  • 消息队列中,消息体需要序列化后才能发送

没有序列化的后果:网络只能传输字节,无法直接传输对象,分布式系统无法通信。

场景三:缓存系统存储

问题:Redis、Memcached 等缓存如何存储 Java 对象?

解决方案:序列化

  • 将 Java 对象序列化为字节数组后存入 Redis

  • 读取时反序列化恢复为对象

  • 大幅减少数据库查询压力

没有序列化的后果:缓存只能存储简单的字符串和数字,无法高效存储复杂对象。

场景四:跨 JVM 进程通信

问题:同一台机器上的两个 Java 进程如何交换对象?

解决方案:序列化

  • 通过 Socket 传输序列化后的对象

  • 通过共享文件传递对象状态

  • JMS 消息服务中的对象消息

没有序列化的后果:进程间只能传递原始字节,无法结构化交换数据。


三、不用序列化行不行?

这是很多初学者都会问的问题,答案是:行,但代价很大,且场景受限。

3.1 如果完全没有序列化,世界会怎样?

让我们设想一个没有序列化的 Java 世界:

1.所有分布式系统都无法工作
  • Dubbo、Spring Cloud 无法进行远程方法调用

  • 微服务架构彻底崩塌,退回单体应用时代

  • 无法构建大型互联网应用

2.对象状态无法持久化
  • 用户登录状态无法保存,每次重启都要重新登录

  • 游戏无法存档,每次打开都从头开始

  • 应用配置无法保存,每次都要重新设置

3.缓存系统效率极低
  • Redis 只能存字符串,无法存对象

  • 每次查询都要手动拼装对象,开发效率极低

  • 系统性能大幅下降

4.开发成本剧增
  • 每个对象都要手动写 "对象→字节" 的转换代码

  • 每个字段类型都要单独处理

  • 极易出错且难以维护

3.2 有没有替代方案?

有!但本质上它们都是 "另一种序列化"

替代方案一:手动拼接字符串
// 手动序列化:拼接成CSV格式
String serialize(User user) {
    return user.getId() + "," + user.getName() + "," + user.getAge();
}

// 手动反序列化:解析字符串
User deserialize(String str) {
    String[] parts = str.split(",");
    return new User(Long.parseLong(parts[0]), parts[1], Integer.parseInt(parts[2]));
}

问题

  • 每个类都要写一遍,重复劳动

  • 处理复杂对象图极其困难

  • 容易出错,类型安全无保障

  • 这就是 "原始的序列化"

替代方案二:JSON/XML 序列化
// Jackson JSON序列化
String json = objectMapper.writeValueAsString(user);
User user = objectMapper.readValue(json, User.class);

注意:这就是序列化!只是序列化的结果是文本格式而非二进制格式。JSON 本质上就是一种序列化协议。

替代方案三:ORM 框架
// MyBatis将对象存入数据库
userMapper.insert(user);
User user = userMapper.selectById(1L);

本质:ORM 框架在底层帮你做了 "对象→数据库字段" 的映射,这也是一种序列化形式。

3.3 什么时候真的可以不用序列化?

只有一种情况:你的对象永远不需要离开当前 JVM 的内存!

  • 对象只在方法内部使用,方法结束就销毁

  • 对象不需要存储到文件、数据库

  • 对象不需要通过网络发送

  • 对象不需要在进程间传递

例如

// 这个临时对象不需要序列化
public void calculate() {
    Calculator calc = new Calculator(); // 方法内临时对象
    int result = calc.add(1, 2);
    System.out.println(result);
} // calc对象在这里被GC回收,从未离开内存

结论:序列化不是 "要不要" 的选择题,而是现代软件开发的 "必需品"。你可以不用 Java 原生序列化,但一定会用到某种形式的序列化。


四、Java 原生序列化机制详解

4.1 基础使用:Serializable 接口

Java 原生序列化非常简单,只需实现java.io.Serializable接口:

import java.io.Serializable;

public class User implements Serializable {
    // 序列化版本号(非常重要!)
    private static final long serialVersionUID = 1L;
    
    private Long id;
    private String name;
    private Integer age;
    
    // 构造方法、getter、setter...
}

Serializable 是一个标记接口,没有任何方法,它的作用只是 "告诉 JVM:这个类可以被序列化"。

4.2 序列化 / 反序列化代码示例

import java.io.*;

public class SerializationDemo {
    
    // 序列化:对象 → 文件
    public static void serialize(Object obj, String filePath) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(filePath))) {
            oos.writeObject(obj);
        }
    }
    
    // 反序列化:文件 → 对象
    public static <T> T deserialize(String filePath) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(filePath))) {
            return (T) ois.readObject();
        }
    }
    
    public static void main(String[] args) throws Exception {
        User user = new User(1L, "张三", 25);
        
        // 序列化到文件
        serialize(user, "user.dat");
        System.out.println("序列化完成");
        
        // 从文件反序列化
        User restoredUser = deserialize("user.dat");
        System.out.println("反序列化结果: " + restoredUser);
    }
}

4.3 serialVersionUID:版本控制的灵魂

为什么需要 serialVersionUID?

  • 序列化时,serialVersionUID 会被写入字节流

  • 反序列化时,JVM 会比较字节流中的 serialVersionUID 和本地类的 serialVersionUID

  • 如果不一致,抛出InvalidClassException

如果不手动指定

  • JVM 会根据类名、字段、方法等自动生成一个 hash 值

  • 只要类结构稍有变化(加个字段、改个方法),serialVersionUID 就会变

  • 导致之前序列化的对象全部无法反序列化!

最佳实践所有可序列化类必须手动指定 serialVersionUID!

// 推荐:手动指定
private static final long serialVersionUID = 1L;

// 不推荐:让JVM自动生成
// (没有这行代码就是自动生成)

4.4 transient 关键字:排除不需要序列化的字段

有些字段不应该被序列化,比如:

  • 敏感信息(密码、密钥)

  • 临时计算值

  • 无法序列化的资源(Socket、Thread、Connection)

使用transient关键字修饰:

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String username;
    
    // 密码不序列化
    private transient String password;
    
    // 数据库连接不序列化
    private transient Connection connection;
    
    // 临时计算值不序列化
    private transient int tempResult;
}

注意

  • transient修饰的字段反序列化后为类型默认值(int→0, 对象→null)

  • static字段无论如何都不会序列化(属于类,不属于对象)

4.5 底层执行流程深度解析

当调用objectOutputStream.writeObject(obj)时,JVM 内部执行以下步骤:

第一步:头部标识写入
  • 写入魔数:0xACED(表示这是 Java 序列化数据)

  • 写入版本号:0x0005(序列化协议版本)

第二步:类元信息写入
  • 写入类的全限定名(如com.example.User

  • 写入 serialVersionUID

  • 写入字段数量和字段信息(名称、类型)

第三步:对象图递归遍历
  • 遍历所有非 transient、非 static 字段

  • 基本类型直接写入值

  • 引用类型递归执行序列化流程

  • 循环引用自动检测处理

第四步:字节流输出
  • 将所有数据按协议格式组装

  • 写入底层输出流


五、主流序列化协议对比

Java 原生序列化虽然方便,但在实际项目中很少使用。让我们对比主流序列化方案:

5.1 序列化协议性能对比表

序列化协议

跨语言

速度

字节体积

安全性

易用性

适用场景

JDK 原生

仅Java

⭐⭐

⭐⭐

⭐(差)

⭐⭐⭐⭐⭐

简单 Java 应用

Kryo

仅Java

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

Java 高性能 RPC

Protobuf

 多语言

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐⭐⭐⭐

⭐⭐

跨语言高性能

Hessian2

多语言

⭐⭐⭐

⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐

Dubbo 默认

JSON

多语言

⭐⭐⭐

⭐⭐⭐⭐⭐

HTTP 接口、调试

5.2 各协议详细分析

JDK 原生序列化(不推荐生产使用)

优点

  • JDK 自带,无需依赖

  • 使用简单,只需实现 Serializable

致命缺点

  1. 不支持跨语言:只能 Java→Java

  2. 性能差:序列化慢、字节体积大(是 Kryo 的 3-10 倍)

  3. 安全漏洞:著名的 Java 反序列化漏洞,可被远程执行代码

  4. 兼容性差:类结构稍变就无法反序列化

Kryo(Java 生态首选)
// Kryo使用示例
Kryo kryo = new Kryo();
kryo.register(User.class);

// 序列化
Output output = new Output(new FileOutputStream("user.bin"));
kryo.writeObject(output, user);
output.close();

// 反序列化
Input input = new Input(new FileInputStream("user.bin"));
User user = kryo.readObject(input, User.class);
input.close();

优点

  • 性能之王:速度最快、体积最小

  • Java 生态成熟,被 Twitter、Storm 等大厂采用

缺点

  • 仅支持 Java 语言

  • 需要提前注册类

适用:Java 后端内部 RPC、高性能场景

 Protobuf(跨语言首选)
// user.proto
syntax = "proto3";
message User {
    int64 id = 1;
    string name = 2;
    int32 age = 3;
}

优点

  • Google 出品,工业级标准

  • 极致性能和压缩率

  • 完美的向前 / 向后兼容性

  • 支持几乎所有主流语言

  • 无安全漏洞

缺点

  • 需要编写.proto 文件

  • 需要编译生成代码

  • 开发流程稍复杂

适用:跨语言通信、对性能要求极高的系统

Hessian2(Dubbo 默认)

优点

  • 跨语言支持

  • 自描述格式,无需预定义 IDL

  • Dubbo 默认序列化

缺点

  • 性能中等

  • 比 Kryo 慢 2-3 倍

适用:Dubbo 生态、需要跨语言的 RPC


六、序列化的常见坑与最佳实践

6.1 常见踩坑点

坑 1:忘记手动指定 serialVersionUID

现象:类加了个字段,之前序列化的数据全挂了

修复:所有序列化类必须手动写serialVersionUID = 1L

坑 2:引用对象未实现 Serializable

现象:抛出NotSerializableException

修复

  • 让引用对象也实现 Serializable

  • 或用transient修饰该字段

坑 3:序列化单例对象

现象:反序列化后创建了新的实例,单例被破坏

修复:添加readResolve()方法

private Object readResolve() { return INSTANCE; // 返回单例实例 }
坑 4:敏感字段未加 transient

现象:密码、密钥被序列化后泄露

修复:所有敏感字段必须加transient

坑 5:反序列化不可信数据

现象:远程代码执行漏洞

修复

  • 绝不反序列化来自不可信源的数据

  • 使用安全的序列化框架(Protobuf)

  • 自定义resolveClass()做白名单校验


一句话总结

序列化不是一项 "可选" 的技术,而是现代软件架构的基石。理解序列化,就是理解分布式系统的数据流动本质。

更多推荐