bigywater.tech
Java · 221

Java Compression:压缩流、缓冲区和数据边界

GZIPOutputStream、Deflater 这些 API 用起来简单,边界却不少:三种格式的头尾、级别的收益、坏数据的各种异常,以及解压时的内存风险。这篇每一项都真实运行并量出数字。

48059 → 45787 字节
JSON 行日志 335563 字节,level 6 与 level 9 的压缩结果
约 1028:1
全零数据的压缩比(解压炸弹)
10 / 27 字节
flush() 后对端可读的字节数:syncFlush=false / true

三种格式:zlib、raw、gzip

实测 · 本机执行输出
  原始数据:JSON 行日志 335563 字节(4000 行)
  zlib 格式(Deflater 默认):48059 字节,前 2 字节 78 9c;raw deflate(nowrap=true):48053 字节;gzip(GZIPOutputStream):48071 字节,前 10 字节 1f 8b 08 00 00 00 00 00 00 ff
  三者的差:zlib − raw = 6(2 字节头 + 4 字节 Adler-32),gzip − raw = 18(10 字节头 + 8 字节 CRC-32 与长度)
  nowrap=true 的 Inflater 还原:335563 字节,是否等于原文 true,finished = true
  用 zlib 模式(nowrap=false)去解 raw deflate → DataFormatException: incorrect header check
  命令行 gzip -6 -c data.json:48081 字节(Java 的 gzip 是 48071 字节);命令行输出头 10 字节 1f 8b 08 08 31 c5 c0 6a 00 03:标志位 08(08 = 带原文件名,多出 10 字节)、OS 字段 03(03 = Unix);Java 写的标志位是 00、OS 字段 ff(未知)
  命令行 gzip -dc 解压 Java 生成的 gz:335563 字节,与原文逐字节比较 cmp 的返回码 0(0 = 一致)

压缩级别与数据类型

实测 · 本机执行输出
  每格:压缩后字节数/单次压缩耗时(预热后 20 次平均)
  JSON 行日志(335563 字节)  L0:335619字节/0.26ms  L1:62382字节/1.06ms  L6:48059字节/2.88ms  L9:45787字节/14.41ms
  随机字节(335563 字节)  L0:335619字节/0.21ms  L1:335674字节/5.12ms  L6:335674字节/5.23ms  L9:335674字节/5.25ms
  已压缩的数据再压一次:gzip(原文)=48071 字节,再 gzip 一次 = 48104 字节(更大)
  8 MiB 全零字节 gzip 后 8175 字节(压缩比约 1026:1)
  Deflater.DEFAULT_COMPRESSION = -1(-1,实际用 6),BEST_SPEED = 1,BEST_COMPRESSION = 9
  预设字典:压缩前 300 字节(首 3 行)→ 无字典 143 字节,带字典 96 字节;解压端需要同一个字典(Inflater.needsDictionary)

解压时的边界

实测 · 本机执行输出
  gzip 截断(去掉最后 100 字节)→ EOFException: Unexpected end of ZLIB input stream
  gzip 中间一个字节被改 → ZipException: Corrupt GZIP trailer
  gzip 的 CRC 字段被改 → ZipException: Corrupt GZIP trailer
  两个 gzip 成员首尾拼接:GZIPInputStream 读出 671126 字节(单个成员 335563 字节)——自动读后续成员
  gzip 后面跟 20 个垃圾字节:读出 335563 字节,没有报错
  不是 gzip 的数据 → ZipException: Not in GZIP format
  空输入 → EOFException: null
  zlib 流截断 → EOFException: Unexpected end of ZLIB input stream

解压炸弹与 flush

实测 · 本机执行输出
  256 MiB 全零 gzip 后只有 254 KiB(压缩比约 1028:1)
  读取时设置 10 MiB 上限:读到 10 MiB 时停止(超限 = true),这时只消耗了不到 254 KiB 的输入
  无上限流式读完:256 MiB,耗时 97 ms(这次只统计字节数、没有保存)
  -Xmx512m 下 readAllBytes 读 256 MiB → OutOfMemoryError: Java heap space
  GZIPOutputStream(syncFlush = false) 写一行并 flush() 之后,管道另一端已经可读的字节数 = 10(10 字节正好是 gzip 头,压缩数据还留在内部缓冲里)
  GZIPOutputStream(syncFlush = true) 写一行并 flush() 之后,管道另一端已经可读的字节数 = 27(对方能立即解出这一行)

分步演示

压缩
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,macOS 自带的 gzip 命令用来交叉验证
    • 没有验证:zstd、brotli、LZ4 等其它压缩格式;多线程压缩;ZipOutputStream 的目录结构与 ZIP64;Inflater 的 JNI 原生内存占用;耗时数字是单机少量测量,只表示相对关系
    京ICP备2026067502号-1京公网安备11010502063487号