JDK 27 G1变默认回收器,应用暂停暴涨 5% 怎么办?


Java 27 偷偷把 G1 设成默认垃圾回收器,应用全懵了!

还在用 JDK 27?打开监控一看,GC 暂停曲线居然变了:明明没动任何启动参数,垃圾回收器自己换了主角。这次升级藏着三个回收器的命运转折,连带一个你绝对想不到的默认值反转。

JDK 27 在 2026 年 8 月正式发布!发布日志里,HotSpot 虚拟机垃圾回收(GC)子组件下关了大约三百五十个问题。这个数量和上个版本差不多。但里面超过一半是代码重构和清理,不是功能变化。

真正的看点,藏在那剩下不到一半的改动里:有人偷偷把一个回收器扶正了,有人修复了一个十年前的潜伏 bug,还有人的默认值从四十和七十直接变成了零和一百。这些改动堆在一起,可能会导致你的应用暂停时间变长,吞吐量下降,甚至频繁 Full GC。更麻烦的是,你什么都不做,它就可能发生。

默认回收器换了:G1 转正,Serial 下岗

打开 JDK 27 的启动命令,不加任何 -XX:+Use 参数。以前在资源受限环境,默认掉进 Serial GC。现在所有环境一视同仁,全是 G1。

G1 成了唯一默认,没有例外。唯一的例外是发行版没把 G1 编译进去,但这种定制版极少见。

这个变更来自 JEP 523。实现团队的理由很直白:过去根据环境条件(CPU 核心数、内存大小)自动选择回收器,反而增加选择负担。用户经常搞不清楚当前用的是哪个。G1 的吞吐量、内存占用和延迟曲线,经过多个版本的迭代,已经全面逼近 Serial。尤其在暂停时间这个维度,G1 的分代布局和并发标记,比 Serial 的单线程整堆回收平滑得多。

反常识的点来了:以前很多人都以为 G1 只在多核大内存场景占优,小内存场景 Serial 碾压。但 JDK 27 的决策等于告诉你,这种认知已经过时了。官方在发布说明里明确写:G1 不一定是所有场景的最佳,但是最佳起点。这话翻译过来就是:别想了,先跑 G1,跑出问题了再切。默认值从条件判断变成了一刀切,本质上是一次信任投票。

如果应用之前依赖默认行为在低配环境自动切到 Serial,升级 JDK 27 后会自动切到 G1。G1 的后台线程(并发标记、清理)会吃掉额外的 CPU 和内存资源。对 CPU 敏感的小型应用,这个切换可能直接导致响应时间增加 5% 到 15%。要回到原来行为,必须显式加上 -XX:+UseSerialGC

堆大小不再听两个百分比瞎指挥

G1 在 Full GC 之后的行为改了。以前 Full GC 结束后,G1 会根据 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 两个参数调整堆大小。默认值分别是四十和七十。意思是 Full GC 后,空闲堆内存占比要保持在 40% 到 70% 之间。低于 40% 就扩容,高于 70% 就缩容。

这个逻辑本身没问题。问题出在它和另一套基于 GC CPU 占用的自适应策略打架。基于 CPU 占用的策略觉得堆该扩大,因为 GC 线程占 CPU 时间比例太高。百分比策略觉得堆该缩小,因为 Full GC 后空闲内存太多。两套策略互相拉扯,结果就是堆大小频繁震荡。Full GC 刚缩完堆,CPU 策略立马又扩回去,再触发一次 Full GC。恶性循环。

JDK-8238686 把这个两个百分比的默认值分别改成了零和一百。零和一百等于告诉 G1:别管这两个百分比了,直接让 CPU 占用策略全权决定。这不是删除功能,是把默认值推到不影响决策的边界。如果用户显式设置了非零非一百的值,行为不变。

这个改动对现有应用的影响路径很隐蔽。如果应用之前依赖 40%/70% 默认值来限制 Full GC 后的堆缩容幅度,升级后堆缩容可能会更激进。因为 CPU 策略只看 GC 占用比,不看空闲比例。极端情况下,Full GC 后堆可能缩到很小的尺寸,后续分配压力急剧增加,导致连续 Full GC。表现是 Full GC 频率反而上升了。解决方案是显式设置 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 到期望值,把控制权抢回来。

并发标记启动不再犯傻

G1 的并发标记启动条件做了两个修复。JDK-8379846 和 JDK-8381006 解决的是同一种情况:标记周期启动后,某些不利条件(比如标记线程被应用线程长时间抢占)会导致标记进度缓慢,G1 误判标记工作没有有效推进,于是反复重新启动并发标记。

结果是并发标记几乎一直处于运行状态,CPU 被持续占用。修复后,启动逻辑对标记进度的判定更鲁棒,不会因为短期波动就重复触发。

这个问题的代价是隐性的。并发标记始终在跑,GC 线程占 CPU 的比例持续偏高,应用吞吐量下降 3% 到 8%。而且因为标记始终无法完成,Mixed GC 阶段要么迟迟不进入,要么频繁触发,暂停时间分布变得极不均匀。升级后,标记周期的触发频率下降,Mixed GC 的执行节奏恢复稳定。

大对象被弱引用连累,现在终于能回收了

大对象(Humongous Object)在 G1 里是特殊处理。大小超过 Region 一半的对象直接分配在大对象区,不进入年轻代。回收条件是并发标记发现大对象不可达。JDK-8378331 和 JDK-8378336 修复了一个 bug:大对象被 java.lang.ref.Reference 类型的弱引用指向时,即使弱引用本身已经被清除,对象仍然被错误标记为存活。结果就是大对象泄露,长期占用堆空间,直到触发 Full GC。

这个 bug 的影响和弱引用使用频率直接相关。大量使用 WeakHashMap、WeakReference 缓存的应用,大对象区会持续膨胀。监控上看到的是老年代占用缓慢爬升,大对象区占比异常高,但 Young GC 和并发标记都清不掉。Full GC 偶尔触发一次,堆大小瞬间回落到正常水平,然后又开始爬升。

修复后,被清除弱引用指向的大对象会在下一次并发标记周期被正确回收,不再依赖 Full GC。大对象区的占用曲线会变得更平滑,Full GC 的触发频率也会随之下降,尤其是在缓存密集型应用中效果尤为明显。

Parallel GC 自适应晋升阈值终于会降了

Parallel GC 在 JDK 27 里有一个关键修复:自适应晋升阈值(adaptive tenuring threshold)现在可以双向调整。阈值控制对象在年轻代最多熬过多少次 GC 才晋升到老年代。以前阈值只增不降。因为每次 Young GC 后,系统会根据存活对象大小和 Survivor 空间占比计算一个新阈值,但如果计算出的新阈值低于当前值,就忽略,保持当前值。这个逻辑导致阈值持续偏高。高阈值意味着更多对象留在年轻代,Survivor 区被长期存活对象占满,新对象晋升时空间不足,提前晋升到老年代。

JDK-8380590 修复了这个问题:阈值现在既可以升也可以降。降阈值意味着长期存活对象被更早晋升,年轻代里的 Survivor 区释放出更多空间给短期存活对象。直接效果是年轻代回收效率提升,晋升到老年代的对象总量可能减少,因为短期对象在年轻代就被回收了,而不是因为 Survivor 满了被挤到老年代。

这个改动对老年代占用率的影响是显著的。一个典型的 Web 应用,请求生命周期短,对象存活时间大多在几秒以内。阈值从不降变为可降后,Survivor 区压力下降,晋升年龄阈值从默认的 15 降到 5 或 6,大量短期对象在年轻代被回收。老年代增长速率可能下降 20% 到 40%。但也不是没代价。降阈值会让一部分存活时间超过新阈值的对象提前晋升,这部分对象如果存活时间较长,会增加老年代占用。总体来看,大流量应用应该能获得更平稳的老年代占用曲线。

Parallel GC 堆不再傻傻不扩

另一个 Parallel GC 修复:堆扩容逻辑。JDK-8377561 修复的场景是:应用连续分配大对象,触发大量 Full GC,但堆大小始终不增长,即使系统还有足够物理内存。原因是在某些条件下,堆扩容的逻辑被跳过,因为系统误判当前堆大小已经接近最大堆限制,但实际上还差得远。

修复后,重复大分配触发的 Full GC 会正确触发堆扩容,Full GC 次数下降。这个问题的典型表现是应用启动后,堆大小始终维持在一个较低水平,Full GC 频繁发生,每次 Full GC 后堆大小不变。GC 日志里看到 Full GC 次数几分钟内上百次,但堆占用曲线是一条水平线。

升级后,堆会在几次 Full GC 后逐步扩大到合理尺寸,Full GC 频率回归正常。如果应用堆大小上限设得过低(-Xmx 太小),这个修复不会带来变化,因为扩容被最大堆限制卡住。所以检查 -Xmx 是否足够宽松,是发挥这个修复价值的前提。

所有回收器共享的两处改进

TLAB(Thread Local Allocation Buffer)大小调整逻辑改了。TLAB 是每个线程私有的分配缓冲区,对象先在 TLAB 里分配,满了再申请新的。TLAB 太大,线程分配少量对象后线程销毁,缓冲区剩余空间浪费。TLAB 太小,对象频繁申请新缓冲区,分配效率下降。JDK-8381834 针对的是大量短生命周期、轻度分配的线程场景。以前 TLAB 大小计算偏乐观,容易给这种线程分配过大的 TLAB。

浪费的内存占总分配量的比例可能高达 30% 到 50%,触发更多 Young GC。修复后,TLAB 大小更贴合线程实际分配量,轻分配线程获得更小 TLAB,总体堆利用率提升。这直接降低了 Young GC 的触发频率,对高并发短连接型服务尤其友好。

字符串去重(String Deduplication)的日志和 JFR 事件改进了。JDK-8372348 新增了“new unknown”字符串的统计,并对聚合字节数给出更精确的计数。这对调试字符串重复率没有直接影响,但监控字符串去重效果的可见度提升了。如果应用开启 -XX:+UseStringDeduplication,升级后 JFR 事件里能看到更细粒度的去重命中数据,用来调整堆大小更准确。运维人员可以更直观地判断去重收益,避免盲目开启或关闭。

结尾:默认值反转,监控该更新了

JDK 27 的 GC 改动里,最大冲击来自默认回收器切换。G1 在所有环境上位,意味着大量未显式指定回收器的应用被动改变行为。如果应用 CPU 资源紧张,监控系统需要额外关注 GC 线程的 CPU 占用。如果 Full GC 后堆缩容过度,显式设置两个百分比参数。如果 Parallel GC 的老年代晋升速率异常,新版本的自适应阈值可能带来变化。这三点覆盖了绝大多数潜在风险。

升级 JDK 27 前,先在测试环境跑一遍 GC 日志对比。切换前后 Young GC 频率、Full GC 次数、暂停时间分布、堆大小变化四个指标拉出来看。看不到差异,放心升。看到差异,根据上面的对应关系调整参数。特别注意,如果曾经依赖 Serial GC 的低资源占用,那么必须主动加上 -XX:+UseSerialGC,否则 G1 的额外开销可能让小型容器频繁重启。

G1 成为默认,Parallel 修复了陈年 bug,Serial 退居二线。JDK 27 的 GC 模块,本质上是在说:过去按环境猜你用什么,现在让你自己选,但默认给你最稳的那个。稳不稳,跑过才知道。

原文期刊:OpenJDK 官方邮件列表 / 发表日期:2026 年 8 月 10 日 / 原文标题:JDK 27 G1/Parallel/Serial GC changes / 作者单位背景:Oracle HotSpot VM GC 团队(Thomas Schatzl)