JDK 27 G1/Parallel/Serial GC 变更
摘要
JDK 27 引入了对 HotSpot 的 stop-the-world 垃圾收集器的显著更改,最重要的是 JEP 523 使 G1 在所有环境中成为默认 GC,同时还包含各种改进、重构和错误修复。
暂无内容
查看缓存全文
缓存时间: 2026/08/13 21:22
# JDK 27 中 G1/Parallel/Serial GC 的变更
来源:https://tschatzl.github.io/2026/08/10/jdk27-g1-serial-parallel-gc-changes.html
OpenJDK 28 即将发布,我想是时候再次讨论 OpenJDK HotSpot VM 中 Stop-The-World 收集器的一些有趣变更了。JDK 27 的 GC 子组件的已解决/已关闭变更完整列表请见这里(https://bugs.openjdk.org/issues/?jql=project%20%3D%20JDK%20AND%20status%20%3D%20Resolved%20AND%20fixVersion%20%3D%20%2227%22%20AND%20component%20%3D%20hotspot%20AND%20Subcomponent%20%3D%20gc%20ORDER%20BY%20summary%20ASC%2C%20status%20DESC),总共约 350 项变更。这个原始数量与上个版本相当;与当时类似,这些数字可能略有虚高,因为 GC 代码中出现了相当数量的重构和清理。其中一个特定领域是替换我们用来指示跨线程共享变量的惯用写法,从使用 `volatile` 和相应的 `AtomicAccess` 方法,改为使用 JDK-8367013(https://bugs.openjdk.org/browse/JDK-8367013)引入的 `Atomic`,以使其使用更加明确。其他值得注意的重构包括 G1 状态机清理(包括一些非常非常古老(https://bugs.openjdk.org/browse/JDK-8080226)的清理)以及其他代码改进。这些约占变更的一半。另有约 35% 的变更涉及错误修复以及一般正确性和健壮性改进。其余部分则涉及 HotSpot STW 收集器的新行为或实质性修订行为——这也是本文接下来要深入探讨的部分 :)
在这些变更中,还包含了 JEP 401:Value Objects(Preview)(https://openjdk.org/jeps/401)的基础设施工作,例如调整对象迭代器以及对值对象的急切回收。
本版本中最具影响力的变更可能是 JEP 523:Make G1 the Default Garbage Collector in All Environments(https://openjdk.org/jeps/523)。现在,如果没有在命令行上显式指定任何垃圾收集算法,G1 就是默认收集器。没有任何例外,默认情况下任何场景都不会使用 Serial GC(也就是说,除非你的 OpenJDK 发行版未包含 G1)。我们认为 G1 非常适合作为默认收集器,而根据晦涩的环境条件选择其他收集器,与其说是优势,不如说是负担。在过去的几个版本中,G1 的吞吐量、内存占用和延迟表现逐步逼近 Serial GC,所以我们觉得是时候切换默认选择了。如果你观察到显著差异,仍然可以使用 `-XX:+UseSerialGC` 选项选择原始的、未修改的 Serial GC。显然在某些情况下,对于你的应用特征而言 Serial GC 是更好的选择——G1 并非在所有地方都是最好的,但我们认为它绝对是最适合起步的。
除了这项变更之外,还有其他几项:
- G1 不再根据(任意的)`-XX:MinHeapFreeRatio` 和 `-XX:MaxHeapFreeRatio` 百分比在 Full GC 后调整堆大小,也就是说,它们的默认值已经被设置为不会影响常规的、基于其他启发式算法的堆大小调整(例如垃圾收集器 CPU 使用率相对于应用程序 CPU 使用率的比例)。JDK-8238686(https://bugs.openjdk.org/browse/JDK-8238686)将它们的默认值分别从 `40%` 和 `70%` 改为 `0%` 和 `100%`。原有的默认值会导致这个启发式算法与其他基于 CPU 使用率的启发式算法相互冲突,造成不必要的 Java 堆大小调整;当一次 Full GC 收缩堆后,其他启发式算法几乎立即撤销这些调整,从而引发性能问题。除此之外,这些标志的功能保持不变。
- 对并发标记的自适应启动进行了改进,使其更能抵抗不利条件,此前这些条件会导致不必要且持续的并发工作(JDK-8379846(https://bugs.openjdk.org/browse/JDK-8379846)、JDK-8381006(https://bugs.openjdk.org/browse/JDK-8381006))。
- 此前,实际上可以被回收的巨型对象(humongous objects)会被弱引用(例如 `java.lang.ref.Reference` 实例)无意中保持在存活状态,已通过 JDK-8378331(https://bugs.openjdk.org/browse/JDK-8378331)和 JDK-8378336(https://bugs.openjdk.org/browse/JDK-8378336)修复。
- 一些小规模的可观察变化包括:G1 Cleanup 暂停不再更新 `MemoryPoolMXBean.getCollectionUsage()`,因为它们不会改变 Java 堆(JDK-8386332(https://bugs.openjdk.org/browse/JDK-8386332))。JDK-8373894(https://bugs.openjdk.org/browse/JDK-8373894):此前,无法找到足够空间进行复制的垃圾回收不会计入垃圾收集器 CPU 使用率。将它们计入可以改善堆大小调整决策。
## Parallel GC
Parallel GC 有一些值得注意的错误修复,可能会提升性能:
- 自适应晋升阈值(adaptive tenuring threshold)决定了对象可以在年轻代中经历多少次年轻代回收。此前,该阈值往往只会升高,极少降低。这意味着持续偏高的阈值可能会让较老的对象占据大量幸存者空间,从而在幸存者空间填满时迫使较年轻的对象过早晋升。通过 JDK-8380590(https://bugs.openjdk.org/browse/JDK-8380590),该阈值也可以降低,从而更快地晋升长生命周期对象,并将可能在短时间内消亡的较年轻对象保留在年轻代中。
- Parallel GC 现在能够在重复的大型分配导致数千次 Full GC 时正确扩展 Java 堆,即使有足够的剩余空间,此前的 Java 堆也不会增长(JDK-8377561(https://bugs.openjdk.org/browse/JDK-8377561))。
## Serial GC
Serial GC 没有特别值得注意的变更——除了在某些场景下不再是默认收集器。
## 所有收集器
所有收集器都有一些有趣的变更:
- TLAB 大小调整现在能更好地处理创建大量短生命周期、分配量较低的线程的应用。这避免了 TLAB 过度分配导致大量浪费,进而增加回收次数(JDK-8381834(https://bugs.openjdk.org/browse/JDK-8381834))。
- JDK-8372348(https://bugs.openjdk.org/browse/JDK-8372348)改进了字符串去重(string deduplication)的日志和 JFR 事件。日志新增了“new unknown”字符串,并提供更有意义的聚合字节数统计。
## 下一步
我们的重点仍然是自动堆大小调整(Automatic Heap Sizing),改进内存管理各个方面的控制,使 G1 能够更智能地响应外部条件和用户意图。
## 感谢……
……一如既往地感谢所有为又一个 JDK 版本做出贡献的人。最后,随着一些重大重构(`Atomic`)的完成,我们有了更多时间来处理其他事项。期待在下一个版本——也许更大的版本——中与大家相见 :)
*Thomas*
相似文章
JDK 27 已发布
JDK 27 作为 Java SE 平台版本 27 的参考实现已发布,包含的更新包括将 G1 设为默认垃圾回收器和 TLS 1.3 的后量子混合密钥交换。
Java 27 发布
Java 27 (JDK 27) 现已正式发布,主要增强功能包括将G1设为默认垃圾回收器、为TLS 1.3提供后量子混合密钥交换以及多种预览功能。
为什么 Arrays.fill 在 G1GC 上慢了 265 倍?
本文探讨了为什么 Arrays.fill 在 G1GC 上比在 ParallelGC 上慢了 265 倍,通过详细的性能分析和汇编代码分析,将问题追溯到 JIT 生成的代码和内存屏障。
Java 27:有哪些新功能?
Java 27 为编程语言引入了新功能和改进。
观察 Go 的新垃圾回收器在堆中的移动
Go 1.26 将 Green Tea 设为默认垃圾回收器,提升了缓存友好性。本文通过 Go 和 C# 可视化堆分配,并讨论了非移动回收器和稀疏页面带来的挑战。