CodeGym /课程 /JAVA 25 SELF /序列化的压缩与性能剖析

序列化的压缩与性能剖析

JAVA 25 SELF
第 45 级 , 课程 4
可用

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. 优化序列化速度

缓冲:为什么需要 BufferedOutputStreamBufferedInputStream

问题:
没有缓冲时,每次 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() + "字节");

结果分析

在测试序列化时,可以明显看到缓冲与压缩的影响。压缩后的文件通常会缩小 210 倍(具体取决于数据结构)。使用缓冲后序列化会明显更快,而压缩会略微降低速度,但常常值得用空间换时间。

结论:对于大数据量务必使用缓冲;如果体积至关重要——再启用压缩。

6. 优化序列化的常见错误

错误 1: 未使用缓冲——序列化大对象会慢好几倍。

错误 2: 序列化不必要或敏感数据(例如密码、临时令牌)——这类字段务必使用 transient

错误 3: 认为压缩总能加速序列化——实际上压缩能减小体积,但可能稍微变慢(尤其在较弱的 CPU 上)。

错误 4: 计时未考虑 JVM 预热与 GC 影响——要做精确基准,请使用 JMH

错误 5: 只比较时间或只比较体积——务必同时关注这两个指标,以便为你的场景选出最佳平衡。

1
调查/小测验
二进制序列化优化第 45 级,课程 4
不可用
二进制序列化优化
二进制序列化优化
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION