1. 引言:为什么要优化序列化?
在现代应用中,序列化无处不在——从网络协议到分布式缓存,再到服务之间的数据交换。
这里序列化的速度与体积都至关重要。如果序列化很慢,应用在保存或加载数据时会“卡顿”,而网络或磁盘也会被白白浪费。如果对象过大,它们会占用更多磁盘空间,网络传输更慢,并给内存与带宽带来额外压力。
典型任务包括:将大型对象图保存到文件或缓存、以尽可能低的延迟在网络中传输对象、以及在多线程系统中快速进行数据的序列化与反序列化。
结论很简单:序列化优化并非“高级特性”,而是高性能、可扩展应用的必备实践。
2. 优化序列化数据的体积
排除不必要的数据:关键字 transient
默认情况下,除被标记为 transient 的字段外,对象的所有字段都会被序列化。如果某个字段无需保存(例如缓存、临时数据、对服务的引用),请将其标记为 transient:
public class User implements Serializable {
private String name;
private transient String sessionToken; // 不会被序列化
}
优点:
- 序列化对象体积更小。
- 文件或网络中不会出现多余/敏感数据。
手动序列化:接口 Externalizable
如果需要对序列化的内容与方式进行完全控制,实现 Externalizable 接口,并在方法 writeExternal/readExternal 中显式描述序列化过程:
public class Person implements Externalizable {
private String name;
private int age;
private transient String secret;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
// 不序列化 secret
}
@Override
public void readExternal(ObjectInput in) throws IOException {
name = in.readUTF();
age = in.readInt();
}
}
优点:
- 只序列化所需字段。
- 可以在不丢失兼容性的情况下更改序列化格式。
压缩:对序列化数据进行压缩
序列化后的对象往往很占空间,尤其是包含大量重复字符串和大型集合时。可以通过压缩来减小体积。
使用 GZIPOutputStream 的示例:
try (ObjectOutputStream out = new ObjectOutputStream(
new GZIPOutputStream(new FileOutputStream("data.gz")))) {
out.writeObject(bigObject);
}
使用 ZipOutputStream 的示例:
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("data.zip"))) {
zip.putNextEntry(new ZipEntry("object"));
ObjectOutputStream out = new ObjectOutputStream(zip);
out.writeObject(bigObject);
out.flush();
zip.closeEntry();
}
优点:
- 文件大小可能成倍缩小(对大型对象图尤为明显)。
- 网络传输流量更小。
缺点:
- 压缩/解压需要额外的时间(CPU)。
3. 优化序列化速度
缓冲:为什么需要 BufferedOutputStream 和 BufferedInputStream
问题:
没有缓冲时,每次 write() 或 read() 都会触发一次对磁盘或网络的系统调用——非常慢!
解决方案:
使用带缓冲的流:
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
try (ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("data.bin")))) {
Object obj = in.readObject();
}
优点:
- 显著加速大对象的读写。
- 减少对磁盘/网络的访问次数。
原理是什么?
缓冲区先在内存中累积数据,然后成块写入,而不是逐字节写入。
快速复制:FileChannel.transferTo
如果需要快速复制大型序列化文件,请使用 NIO 的 transferTo 方法:
try (FileChannel src = new FileInputStream("data.bin").getChannel();
FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
src.transferTo(0, src.size(), dest);
}
优点:
- 拷贝在操作系统层完成,绕过 Java 中的额外缓冲——对大文件非常快。
4. 序列化的性能分析
简单计时:System.nanoTime()
用于快速评估序列化性能,可以使用 System.nanoTime():
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
long end = System.nanoTime();
System.out.println("序列化耗时:" + (end - start) / 1_000_000 + "毫秒");
优点:
- 简单且快速。
- 可以比较不同方案(有无缓冲、是否压缩等)。
缺点:
- 结果可能因 GC 与后台进程而抖动。
- 不适合比较微小差异。
精确的性能分析:JMH (Java Microbenchmark Harness)
若需更精确的测量,请使用 JMH——一款用于微基准的专用库。
简单基准示例:
@Benchmark
public void serializeWithBuffer() throws Exception {
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
}
优点:
- 考虑 JVM 预热、GC 影响与操作系统“噪声”。
- 提供可靠、可复现的结果。
缺点:
- 需要一定配置并理解 JMH 的方法论。
- 对于“肉眼比较”来说可能过度。
5. 实践:比较序列化的时间与体积
让我们做个小实验:以不同方式序列化一个大型对象图(例如包含 100_000 个对象且带嵌套集合的列表),并比较耗时与文件大小。
无缓冲、无压缩的序列化
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("无缓冲:" + (end - start) / 1_000_000 + "毫秒,大小:" +
new File("data1.bin").length() + "字节");
带缓冲的序列化
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data2.bin")))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("带缓冲:" + (end - start) / 1_000_000 + "毫秒,大小:" +
new File("data2.bin").length() + "字节");
带压缩(GZIP)的序列化
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
new GZIPOutputStream(new FileOutputStream("data3.gz")))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("带压缩:" + (end - start) / 1_000_000 + "毫秒,大小:" +
new File("data3.gz").length() + "字节");
结果分析
在测试序列化时,可以明显看到缓冲与压缩的影响。压缩后的文件通常会缩小 2–10 倍(具体取决于数据结构)。使用缓冲后序列化会明显更快,而压缩会略微降低速度,但常常值得用空间换时间。
结论:对于大数据量务必使用缓冲;如果体积至关重要——再启用压缩。
6. 优化序列化的常见错误
错误 1: 未使用缓冲——序列化大对象会慢好几倍。
错误 2: 序列化不必要或敏感数据(例如密码、临时令牌)——这类字段务必使用 transient。
错误 3: 认为压缩总能加速序列化——实际上压缩能减小体积,但可能稍微变慢(尤其在较弱的 CPU 上)。
错误 4: 计时未考虑 JVM 预热与 GC 影响——要做精确基准,请使用 JMH。
错误 5: 只比较时间或只比较体积——务必同时关注这两个指标,以便为你的场景选出最佳平衡。
GO TO FULL VERSION