Java基础、中级、高级、架构面试资料

JDK 25 的 G1GC 存在静默数据损坏 Bug,携程踩坑

JAVA herman 9浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

前不久,我写了 JDK 25.0.3 的新版本发布文章,不少人表示 JDK 25 目前用的还少,更有网友表示还在使用 Java 8。一边是为了稳定性,能不升级就不升级;另一边是迫不及待,能升级则升级。

尝鲜的人确实少,但我今天看到了一篇文章,说携程已经踩了 JDK 25 的坑了。而且这个坑,JDK 25 前 3 个大版本全军覆没,25.0.0、25.0.1、25.0.2 均存在 G1GC 静默数据损坏 Bug。这个 Bug 让携程在 Parquet 文件在写入时就已经“烂掉”了。

我本以为,这个 bug 只有携程遇到了,谁知查了一些社区资料,发现也有不少老外企业遇到了。它能让大家的的数据,可能在写入那一刻就已经“死了”(不正确了)。

其实,早在 4 月份,JDK 25.0.3 就发布了。但在这之前,不少尝鲜的头部企业踩了坑,携程就是其中之一。由此,它们触发了 bug,揭开了一个让全 Java 社区倒吸一口凉气的真相。

G1 垃圾回收器,在特定条件下会“偷走”你的对象,而你的程序对此一无所知。

需要注意的是,JDK 25 默认 GC 就是 G1。并且,此 bug 的影响范围是 JDK 25.0.0、25.0.1、25.0.2 这 3 个已发布的版本。

故障问题还很能复现,没有 OOM,没有 crash,没有异常。携程的 Spark 任务正常跑完,Parquet 文件顺利落盘,CRC 校验全部通过。直到下游作业读取时,Zstd 解压报出 Corrupted block detected,数据已经不可恢复。

这不是压缩库的 bug,不是存储介质的故障,甚至不是携程业务代码的问题。这是 JDK 25 G1GC 的一个底层缺陷,静默地、随机地、不可逆地损坏你的数据

本意是白嫖 JEP 519

这个故事背景,听一听就很有画面感。

下游系统读取文件异常了,检查了半天,不知道是哪里的问题。好不容易排查出来是上游给的文件出问题了,而上游仔细查看了相关代码,看不出任何毛病。然后系统也没有 OOM,日志也正常,总之就是出鬼了。

这就是为了“白嫖” JEP 519 的性能,携程踩进了坑。此时此刻,我想起了那啥,可不就是为了这瓶醋包了这顿饺子嘛。

事故或事情要讲情况,还得从 JDK 25 的 JEP 519 Compact Object Headers(紧凑对象头)说起。

这个特性堪称 Java 25 最诱人的“性能白嫖”,只需加一个 JVM 参数即可。

-XX:+UseCompactObjectHeaders

有了这个参数就能实现。

  • 堆内存节省最高 22%
  • GC 频率降低约 15%
  • Amazon 生产环境实测 CPU 降低 30%

要知道,携程大数据平台运行着大规模的 Spark、Flink 计算集群,为了充分利用 JDK 25 LTS 在内存效率与运行性能上的优势,团队启动了升级计划,完成了多个引擎对 JDK 25 的适配改造,开始向生产环境灰度推进。

初衷是好的,坑也是真的深。本想节省不少成本呢,结果说多了都是泪呀。

文章配图参见 https://mp.weixin.qq.com/s/Y8sFQgANEz-W8DDZcwGdgQ

诡异的现象

携程在灰度推进阶段,有程序员反馈出现异常。

  • 实时任务:Flink 写入 Paimon 的 Parquet 文件,部分读取失败
  • 离线任务:Spark 写入的 ORC、Parquet 文件,也存在部分读取失败

核心报错集中为 Zstd 解压相关异常:

Caused by: com.github.luben.zstd.ZstdException: Src size is incorrect

Caused by: java.io.IOException: Decompression error: Destination buffer is too small

Caused by: java.io.IOException: Decompression error: Corrupted block detected

最诡异的是,写入流程完全正常,没有任何报错。CRC 校验也全部通过。损坏只在下游读取时才暴露。

这意味着什么呢?意味着数据在写入的那一刻就已经“烂”了或坏了,但没有任何人知道

排查之路

出现问题后,携程团队展开了系统性的排查,从怀疑一切,到锁定 JDK 本身。

排除存储介质

Parquet 默认开启 parquet.page.write-checksum.enabled=true,压缩后的数据通过 CRC32 计算校验值。开启读取校验后执行校验,结果无报错

这说明:压缩后写入文件的字节流与读取时一致,排除 HDFS 等存储介质导致的数据损坏

第一步,它们都没想起是最近灰度的 JDK 有问题,大家先相信的还是 JAVA。

排除压缩库

紧接着,他们发现 ORC 2.0+ 的 Zstd 压缩支持两种实现。

  • airlift aircompressor 的纯 Java 实现
  • zstd-jni 的 C 代码实现(默认,性能更优)

切换到纯 Java 实现后,问题不再复现。因此,他们初步怀疑 zstd-jni 存在适配问题。

排除紧凑对象头

于是,他们决定关闭 -XX:+UseCompactObjectHeaders,然后问题仍然可以复现。排除 JEP 519 的影响。

JDK 版本二分

接下来,他们开始了 JDK 版本方面的排查。

  • JDK 21、JDK 23、JDK 24 最后一个版本均无问题
  • JDK 25.0.0、25.0.1、25.0.2 均可复现
  • JDK 26、JDK 27 无问题(但 Spark 兼容性未完成测试)

这说明问题出在 JDK 24 到 JDK 25 的改动中。

GC 算法锁定

观察到损坏的列基本都是大文本字符串,Spark GC 耗时异常。尝试更换 GC 算法。

配置数据损坏?
JDK 25 + G1GC(默认)损坏
JDK 25 + ParallelGC正常
JDK 25 + ZGC正常
JDK 21 + G1GC正常

到此,问题被锁定了,是 JDK 25 + G1GC 捣的鬼。

一个 Commit 引入的致命 Bug

为了精确定位引入问题的 Commit,携程团队借助 GitHub Agents 的能力,同时使用 AI vibe 了一个 JDK 25 编译 Workflow,支持指定 Commit ID 编译,并解决了 CentOS 7 低版本 glibc 的兼容问题。

通过二分查找,最终锁定引入 Bug 的 Commit:JDK-8343782

G1: Use one G1CardSet instance for multiple old gen regions
Commit: 86cec4ea,Resolved In Build: b10

修复 Bug 的 Commit:JDK-8370807。

G1: Improve region attribute table method naming
Commit: 17fd801b,Resolved In Build: b22

虽然修复 Commit 的标题看起来只是“改进方法命名”,但关键改动在于,在注册老年代区域进行回收时,正确传递了 r->has_pinned_objects() 属性。

G1GC 为何偷走对象

要搞懂这其中的秘密,我们先来看看什么是 JNI Critical Pinning?

JNI Critical Pinning

简单来说,Java 通过 JNI(Java Native Interface)调用 Native 代码时,如果 Native 代码需要直接操作 Java 数组的内存,会使用一对关键函数:

// 获取数组的内存指针,并“固定”该对象
void* GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);

// 释放固定
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

“固定”(Pin)的含义是,告诉 GC,这个对象正在被我(Native 代码)直接读写,你不能移动它的物理内存位置。

当一个数组被 GetPrimitiveArrayCritical 锁定时,它所在的 heap region 会被标记为 has_pinned_objects

问题出在哪?

JDK-8343782 的初衷是优化 G1 GC 的内存占用,使用一个 G1CardSet 实例管理多个老年代区域。但这个优化引入了一个致命缺陷。

在 Optional Evacuation(可选回收)阶段,G1 忽略了对象的 Pinned 状态,错误地移动了被 JNI 临界区锁定的对象。

这对 zstd-jni 的影响链路如下。

  1. zstd-jni 通过 GetPrimitiveArrayCritical 锁定 Java 数组,获取内存指针
  2. Native 的 C 代码开始向该地址写入压缩后的数据
  3. G1 GC 在 Optional Evacuation 阶段,强行移动了这个 Java 数组的内存位置
  4. C 层的压缩逻辑把数据写到了旧的、已经被废弃的地址
  5. 因为直接操作内存,没有任何异常抛出
  6. 最终落盘的 Java 字节数组里的数据已经完全错乱
  7. 下游读取时,Zstd 解压因格式不符而报错

说白了,就是 pinned 标志丢失 → GC 移动了不该移动的对象 → JNI 裸指针指向废弃内存 → 最终触发了数据静默损坏

为什么叫静默损坏?

这是这个 Bug 最可怕的地方在于无任何异常日志等信息。

特征表现
写入时无任何异常,程序正常跑完
CRC 校验全部通过(因为写入和读取的字节流一致)
文件格式看起来正常,只是内容已损坏
报错时机只有下游读取/解压时才暴露
数据恢复不可恢复,因为原始数据已经被覆盖

影响面

看到这里,就知道了,这个 bug 的影响面远不止 zstd-jni。

该 Bug 的根本原因是 G1 GC 在 Optional Evacuation 阶段错误移动了被 JNI 临界区锁定的对象。因此,凡是通过 GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical 直接操作 Java 数组内存的场景,均存在触发风险。

受影响的组件/库包括但不限于。

  • zstd-jni:Zstd 压缩(ORC、Parquet、Kafka 消息体等大量使用)
  • JDK 内置 Zip/Deflate 库:java.util.zip.Deflater / Inflater(同样基于 JNI 实现,已验证可复现)
  • 其他调用 Native 压缩/加密/数学运算库等各种场景

携程团队还热心的验证了 JDK 自带的 Zip 压缩库同样可复现该问题。

Caused by: java.io.IOException: Bad compression data
Caused by: java.util.zip.DataFormatException: invalid stored block lengths

修复与规避

官方修复

OpenJDK 社区在接到报告后高度重视,经过多轮讨论和验证,最终实现了 backport。

  • JDK-8377811: G1: Optional Evacuations may evacuate pinned objects
  • 该修复已包含在 JDK 25.0.3 中(2026 年 4 月 21 日发布)

临时规避方案

如果有读者正在使用 JDK 25.0.0、25.0.1、25.0.2,则建议进行以下选择进行修复。

方案操作
升级尽快升级至 JDK 25.0.3+
换 GC使用 -XX:+UseParallelGC-XX:+UseZGC 替代 G1GC
降级回退至 JDK 21 LTS

关于 JEP 519 紧凑对象头

这个特性本身没有问题,携程的排查也证实了这一点。但它是一个需要显式开启的参数,不是默认行为。如果你开启了它,请确保你的 JDK 版本已经修复了上述 G1GC 的 bug。

这个参数,将来说不定会成为默认。算了,将来的事将来再说吧。

写在最后

JDK 25 是一个充满诱惑的 LTS 版本,紧凑对象头、虚拟线程成熟、Shenandoah 分代 GC …… 每一个特性都在说“快来用我”。

但携程的这次踩坑告诉我们,新版本的 .0、.1、.2 补丁,往往是踩雷高发区

G1GC 的这个 bug,从 JDK-8343782 引入到 JDK-8370807 修复,横跨了 JDK 25 的三个小版本。如果不是像携程这样的一批头部企业在生产环境大规模灰度中及时发现并推动社区修复,这个“静默数据损坏”的隐患可能会潜伏更久,影响更广。

技术选型没有银弹,升级路上永远有坑。保持敬畏,保持验证,保持对底层原理的好奇心,这才是工程师的护城河。

Java 的进步都是由少数人推动的,我们多用,让 Java 再次伟大!

参考资料

  • https://bugs.openjdk.org/browse/JDK-8343782
  • https://bugs.openjdk.org/browse/JDK-8370807
  • https://juejin.cn/post/7638635597335658547
  • https://bugs.openjdk.org/browse/JDK-8377811
  • https://openjdk.org/jeps/519

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » JDK 25 的 G1GC 存在静默数据损坏 Bug,携程踩坑