Administrator
发布于 2024-06-11 / 1813 阅读
15

生产环境 JDK 21 升级后的 GC 表现复盘

我们一个网关服务,平时 P99 延迟 40ms 很稳,但每天凌晨批量任务后总有几次 200ms+ 的卡顿尖刺。G1 的 Mixed GC 停顿在堆大时是元凶。JDK 21 上 ZGC 已经成熟,我决定赌一把把它换上。

切换决策:先看停顿从哪来

先用 JFR 抓了一周 GC 数据。G1 在 16G 堆下,Young GC 停顿约 30ms,但 Mixed GC(回收老年代)偶发到 180ms,且随存活对象增多而抖动。网关对尾延迟敏感——一个 200ms 的 GC 停顿,会直接拖慢那一波所有请求。ZGC 的承诺是"停顿不随堆大小增长",官方说 < 10ms,这正是我们想要的。

切换过程

JDK 21 上开启 ZGC 只需换 GC 参数,业务代码零改:

-XX:+UseZGC
-XX:MaxHeapSize=16g
-XX:SoftMaxHeapSize=12g   # 让 ZGC 在空闲时主动把堆缩回来
-XX:+ZGenerational        # 分代 ZGC,JDK 21 默认开启

注意 ZGenerational:JDK 21 的分代 ZGC 比非分代吞吐量高约 15%,我们一开始没开,Young 对象回收效率低,换上分代后吞吐回正。

延迟改善实测

指标G1ZGC变化
GC 最大停顿182ms8ms-96%
GC 平均停顿31ms1.2ms-96%
请求 P99 延迟212ms52ms-75%
吞吐(QPS)基准 100%约 94%-6%

尾延迟塌方消失,P99 从 212ms 降到 52ms。代价是吞吐掉 6%,CPU 占用涨约 10%(ZGC 并发标记要算力)。对网关而言,尾延迟比吞吐重要,这 6% 换来的是体感的质的提升。

踩坑记录

  • 堆不缩:没设 SoftMaxHeapSize 时,ZGC 把堆顶到 16G 不下来,K8s limit 边缘游走,差点 OOMKilled。设了软上限后稳定在 12-13G;
  • 分配速率:ZGC 对"分配风暴"敏感,某次营销活动瞬间新建大量短命对象,Allocation Stall 让个别请求卡了 50ms。加 -XX:ConcGCThreads 提并发标记线程数缓解;
  • 监控盲区:原 Prometheus 的 GC 面板是按 G1 指标做的,切 ZGC 后一堆指标为空。改用 ZGC 专属的 jdk.ZGarbageCollection JFR 事件导出,重新配了看板。

哪些场景不适合 ZGC

不是所有服务都该换。我们评估的标准:

  • 堆 < 4G、停顿本来就不大:G1 够用,换 ZGC 收益小;
  • CPU 极度紧张:ZGC 吃并发算力,CPU 余量不足会反噬;
  • 批处理离线任务:吞吐优先,G1 或 Parallel 更合适。

监控指标与告警配置

切 ZGC 后监控得重配。我们重点盯三个指标:ZCollectionCount(GC 次数)、Allocation Stall 次数、以及堆的 SoftMax 实际使用。告警这样设:单次 GC 停顿 > 10ms 且持续 5 分钟告警(理论上不该发生,触发即异常),Allocation Stall 计数 > 0 直接电话告警(说明分配速率压垮并发回收)。还配了"堆实际占用持续贴近 limit 的 90%"的预警,提前发现内存泄漏或 SoftMax 失效。JFR 我们开了连续飞行记录(-XX:StartFlightRecording),保留最近 12 小时,出事能回放 GC 全过程,比看聚合指标管用得多。

小结

从 G1 切到 ZGC,核心收益是"停顿与堆大小解耦",把网关尾延迟尖刺从 182ms 压到 8ms,P99 降 75%。代价是 6% 吞吐和略高 CPU,对延迟敏感服务很划算。踩坑集中在堆软上限、分配风暴和监控适配。决策口诀:延迟敏感、堆大、CPU 有余,上 ZGC;吞吐优先、堆小、CPU 紧,留 G1。别盲目追新,用 JFR 数据说话。

参考