Skip to main content

JEP 523:在所有环境中将 G1 作为默认垃圾收集器

引言

垃圾收集器的选择,往往是 Java 应用里最"隐形"的一个配置。绝大多数开发者从来没有显式指定过它——他们信任 JVM 会替自己做出合理的默认选择。JEP 523 关注的正是这个默认选择背后的逻辑,并把它从一套诞生于二十年前的启发式规则,改成了一条简单、统一、可预测的策略:无论跑在什么机器上,默认都用 G1。

今天的默认值是怎么选出来的

在 Java 27 之前,JVM 并不会一视同仁地对待每台机器。它会在启动时"打量"一下宿主环境,然后根据机器的规格来决定给你哪个垃圾收集器。这套逻辑被称为服务器级机器判定(server-class machine detection)

  • JVM 会把主机判定为「服务器级(server-class)」或「客户端级(client-class)」两类之一。
  • 判定的门槛大致是:2 个以上的 CPU,且 1792 MB 以上的物理内存。达到这个门槛的被视为服务器级,选择 G1。
  • 低于这个规格的一切机器,一律回退到 Serial GC——一个单线程、停顿随堆增大而线性变长的收集器。

问题的根源在于,这套启发式规则诞生于单核桌面机占主流的年代。在那个时代,这个判断是合理的:只有真正的"大机器"(服务器)才需要 G1 这类为吞吐和低延迟设计的并发收集器,而普通桌面机跑个 Serial GC 也就够了。

为什么这套规则不再适用

云原生与容器化彻底改变了"机器"的含义。今天所谓的"小机器",已经不再是老式桌面机,而是成千上万个运行在 Kubernetes 上的容器实例:

  • 容器常常只分到 1 个 CPU、不足 1792 MB 内存,于是按老规则,它们统统拿到了 Serial GC。
  • 两个几乎一模一样的部署,可能跑在两种完全不同的收集器上。你的微服务分到 1 个 CPU,用 Serial GC;隔壁那个几乎相同的服务分到 2 个 CPU,用的却是 G1。同样的代码、不同的收集器。
  • Serial GC 的停顿时间随堆增大而变长,这对延迟敏感的在线服务是致命的——一次长停顿就可能拖垮 P99 延迟。
  • 更隐蔽的是,调优建议、基准测试、性能文档往往只对其中一种收集器成立,却被无差别地套用到另一种上。排查性能问题因此变成了一场噩梦:你以为在对比两个相同环境,实际上底层跑的是两套完全不同的内存管理机制。

JEP 523 改变了什么

解决方案朴素得近乎"反高潮":不管机器规格如何,一律默认使用 G1。

  • **G1 成为所有环境中的默认垃圾收集器。**从笔记本到单 CPU 容器,再到 64 核大服务器,默认拿到的都是同一个收集器。
  • **服务器级机器判定不再用于选择收集器。**那套二十年前的启发式规则从"决定用哪个 GC"的决策链里被移除了。
  • **Serial GC 并没有消失。**它依然被完整支持,只是不再是任何机器的自动默认。如果你确实需要它,一个 -XX:+UseSerialGC 参数就能切回去。
  • **G1 本身没有任何改动。**这一点很关键:JEP 523 改的只是"默认会拿到哪个收集器"这一个决策,收集器的实现代码一行都没动。

权衡:得到什么,又要留意什么

任何默认值的改变都是一种权衡。总体来看,G1 对绝大多数负载都是更好的默认选择,因为它的停顿时间更短、更可预测。

带来的好处:

  • **小堆上的停顿更短、更可预测。**G1 的并发、分区式设计意味着即使堆增长,停顿也能维持在一个可控范围。
  • **开发、CI 与生产用的是同一个收集器。**这消除了一整类"在我机器上是好的"式的性能差异。
  • G1 的低延迟设计在所有环境里都能用上,不再是"大机器"的专属。

需要留意的地方:

  • **G1 的内存开销略大一些。**它需要额外的数据结构来维护分区(region)管理,这会占用一小部分内存。
  • **在极小、极短命、单 CPU 的负载上,Serial GC 仍可能更快。**比如一次性运行几秒钟的批处理小任务,Serial 的简单性反而是优势。
  • **先测量,再决定。**如果你考虑改回 -XX:+UseSerialGC,务必基于实测数据,而不是直觉。

结论

从 Java 27 起,无论你的部署目标是笔记本、单 CPU 容器,还是 64 核服务器,默认拿到的都是 G1。这次改动没有引入任何新特性,也没有改动 G1 本身——它只是修正了一个"默认值",却消除了困扰整个生态多年的一个隐性差异来源。从此,社区积累的调优经验、基准数据和最佳实践,终于能在所有环境里一致地适用。