发布时间:2026/7/11 23:56:29
JDK 17 ZGC 升级导致 P99 延迟飙升 10 倍?GC 选型与调优实战 叙事框架现象 → 排查过程 → 根因 → 修复 → 预防问题现象本文记录了一次从 JDK 11 升级到 JDK 17 过程中因 ZGC 默认启用导致支付服务 P99 延迟从 50ms 飙升到 534ms 的完整排查与解决过程。问题根因是 ZGC 在高并发对象分配场景下的 Allocation Stall 机制。最终方案是回退到 G1 并配置 MaxGCPauseMillis20恢复 P99 至 45ms。文章深入分析 ZGC 与 G1 的性能表现差异。排查过程第一步确认 GC 状态和策略先看进程启动参数确认当前用了什么 GCjps-lvm|greppay-service输出显示参数里有-XX:UseZGC但启动脚本里并没有显式写过这个参数。这是因为JDK 17 在某些平台上默认 GC 变成了 ZGCJDK 11 默认是 G1升级时没加显式 GC 参数系统自动选择了 ZGC。接下来用jstat -gcutil看 GC 运行状态这里有几个非常关键的信号信号一CGC 列有值YGC 和 FGC 为空。在 G1 或 Parallel GC 下jstat -gcutil会显示 YGCYoung GC 次数、YGCYoung GC 耗时、FGCFull GC 次数、FGCTFull GC 耗时。但在 ZGC 下这些列全是空的——因为 ZGC 没有传统的 Young/Full GC 概念取而代之的是CGCConcurrent GC所有回收都是并发的。CGC 从第 8 次到第 15 次8 秒内发生了8 次并发 GC——意味着不到 1 秒就触发一次频率极高。信号二EEden 区使用率从 52.3% 快速攀升到 95.1%。这是高分配率的直接证据——新对象创建速度极快Eden 区在几秒内就被填满。ZGC 感知到高分配率后会启动并发标记但每次并发标记还没完成新的分配请求又在堆积。信号三LGCCLast GC Cause显示 “Allocation Stall”。这是最直接的证据jstat -gccause输出的 LGCC上次 GC 原因和 GCC当前 GC 原因都是“ZGC Allocation Stall”。什么是 Allocation StallZGC 是并发 GC大部分阶段不暂停应用线程。但当应用分配内存的速度超过了 ZGC 并发回收的速度堆内存就会不断消耗直到临界值。此时 ZGC 别无选择——它必须暂停分配线程等并发回收赶上来。这个暂停就是 Allocation Stall。jstat -gccause连续 3 次采样都显示 Allocation Stall说明问题在持续恶化。第二步GC 日志定量分析GC 日志里藏着最直接的数据。先看 Allocation Stall 的分布几个关键分析Allocation Stall 单次时长分布最后 5 个最大值312ms, 345ms, 423ms, 567ms, 892ms这组数据说明最严重的 Allocation Stall 接近 900ms。对于上游设置了 500ms 超时的调用方来说一个 892ms 的停顿意味着这笔请求必然超时。Allocation Stall 总量统计2847次, 累计423.18s, 平均148.62ms在不到 20 分钟的窗口内应用有 423 秒——也就是 7 分钟——被花在了 Allocation Stall 上。这意味着该时间段内应用有超过35% 的时间处于分配暂停状态。对比 ZGC 官方宣称的「STW 不超过 1ms」——实际生产中在特定场景下可以达到近 900ms相差近三个数量级。这不是 ZGC 本身的问题而是选型与应用场景不匹配。非 Allocation Stall 的 GC 日志行数达到 18923 行——意味着不到 20 分钟就产生了 18K 条 GC 日志进一步印证了 GC 活动的极度频繁。第三步线程状态分析——看清连锁反应停顿时间这么长业务线程的直观感受就是——卡死了。从 jstack 的快照可以看出8 个业务线程biz-0 到 biz-7中只有 1 个biz-4处于 RUNNABLE 状态其余 7 个要么 BLOCKED 要么 TIMED_WAITING。值得注意的是 biz-3 的堆栈biz-3#42 daemon prio5 tid0x... nid0xab12java.lang.Thread.State: BLOCKED at java.util.HashMap.computeIfAbsent(...)at...SimpleCacheManager.getCache(...)at...CacheInterceptor.invoke(...)at...ReconciliationServiceImpl.process(...)这个线程在等待 HashMap 上的锁而这个锁本来应该很快就释放的。为什么被持有了那么久因为持有锁的线程正好在 GC 停顿期间被挂起了——GC 停顿导致锁持有时间被无限拉长其他等锁的线程全部阻塞。这就是GC 停顿的连锁放大效应GC 本身的停顿是 100ms 级但通过锁竞争、线程排队、请求堆积最终表现为客户端看到的 P99 变成 500ms。ZGC 的线程组ZGC Worker#0-#5也显示 CPU 占用很高987ms~1234ms说明 GC 线程本身已经满负荷运转。第四步控制变量——ZGC vs G1 对比测试确认问题与 ZGC 强相关后在压测环境用同一份代码、同一压力做了对比测试对比结果非常说明问题GC 配置吞吐量req/5min慢请求率100msZGC无参8128.4%-11.8%G1MaxGCPauseMillis5012070.3%-0.5%G1MaxGCPauseMillis20, G1HeapRegionSize4m12410.1%-0.2%ParallelGC基线12890.2%这个结果与很多人「ZGC 一定比 G1 快」的直觉相反。分析原因ZGC 在 2G 堆下的劣势ZGC 是为大堆64G设计的。它的并发标记-整理算法依赖并发阶段与业务线程并行执行但需要额外的内存屏障和活跃数据映射。在 2G 小堆下ZGC 的并发优势完全没有发挥空间——堆太小一次并发标记很快就能完成但并发标记的启动条件在高分配率下被频繁触发反而带来了 Allocation Stall 的惩罚。G1 的优势G1 在中小堆8G下表现稳定。它通过分代收集 停顿预测模型来控制每次 GC 的暂停时间。MaxGCPauseMillis20让 G1 将每次 GC 停顿控制在 20ms 以内虽然单次停顿是 ZGC 宣传值的 20 倍但G1 在高分配率下不会出现 Allocation Stall 这种百毫秒级的阻塞——这是它在这个场景下胜出的关键。调优后的 G1 吞吐量比 ZGC 高出50%慢请求率从 10% 降到 0.1%。根因分析团队在群里对整个问题的起因也做了复盘根因可以拆为三层1. 直接原因JDK 17 默认 GC 变更JDK 17 在某些平台和配置下默认 GC 变成了 ZGC具体决策取决于 GC ergonomics。升级时没有在启动脚本中显式添加-XX:UseG1GC或-XX:UseZGC导致系统自动选择了 ZGC。这个变化在 JDK 发行说明中以小字形式存在容易被忽略。2. 根本原因ZGC 与业务场景不匹配ZGC 的核心设计假设是「堆内存足够大让并发回收有充足时间追赶分配」。它的适用场景大堆64G建议 128G大堆给并发 GC 足够的缓冲空间减少 Allocation Stall 概率中低分配率分配速度不超过并发标记的推进速度低延迟优先吞吐量可让步ZGC 的并发阶段需要额外 CPU 开销写屏障、重映射吞吐量可能不如 G1而支付对账服务的特点是高分配率每笔对账创建大量中间对象DTO、缓存条目、差异记录中等堆2G小堆下 ZGC 并发优势归零RT 敏感但不极端P99 50ms 的要求 G1 轻松满足3. 测试覆盖不足的原因压测环境用低流量验证了三天ZGC 在低分配率下表现完美——GC 日志里几乎看不到 Allocation Stall。因为 ZGC 在空闲时会释放所有已分配内存测试的分配率远未达到触发 Allocation Stall 的阈值。这就引出一个教训对于 GC 选型验证不能只用低流量跑「功能测试」必须用接近生产峰值的压力暴露 GC 行为差异。修复方案修复的核心是两条显式指定 GC 参数调优。对比修复前后的配置修复后的配置JAVA_OPTS-Xms4g -Xmx4gJAVA_OPTS$JAVA_OPTS-XX:UseG1GCJAVA_OPTS$JAVA_OPTS-XX:MaxGCPauseMillis20JAVA_OPTS$JAVA_OPTS-XX:G1HeapRegionSize4mJAVA_OPTS$JAVA_OPTS-XX:HeapDumpOnOutOfMemoryError三个参数调整的考量-XX:UseG1GC核心显式用 G1 替代 ZGC。这是本次修复最关键的改动——不再依赖默认 GC 策略-XX:MaxGCPauseMillis20从 50 下调G1 的停顿预测模型会力争将每次 GC 停顿控制在 20ms 以内。值越小G1 调用的 Young GC 越频繁但每次停顿越短。对于 50ms P99 要求的服务20ms 更安全-XX:G1HeapRegionSize4m优化 Region 大小G1 将堆划分为等大的 Region。2G 堆下4m Region 创建 512 个 Region。Region 太小会增加 Remembered Set 维护开销太大则精细度不够。4m 在 2-4G 堆区间是平衡点堆从 2G 扩到 4G稍大的堆让 G1 的并发标记周期更从容减少并发标记失败退化为 Full GC 的概率验证结果上线后监控显示5 分钟内P99 RT 从 534ms 回落到 80ms15 分钟内P99 RT 稳定在 42-55ms恢复到升级前的水平慢请求率从 10% 降至 0.1-0.2%GC 频率Young GC 约 3-5 秒一次每次停顿 8-15ms无 Full GC避坑建议JDK 升级清单必须包含 GC 验证JDK 11→17 的默认 GC 可能从 G1 变为 ZGC。升级前后执行java -XX:PrintCommandLineFlags -version确认默认 GC并在启动脚本中显式指定GC 参数压测流量必须逼近生产峰值ZGC 在低分配率下表现完美但高分配率下可能出现百毫秒级 Allocation Stall。压测场景至少要达到生产峰值的 80%监控 GC 停顿类型除了常规的 GC 次数和耗时监控还要关注 ZGC 的 Allocation Stall、G1 的 Full GC 次数、Safepoint 停顿等。这些指标往往在默认的 GC 监控模板中被遗漏不要默认 ZGC 一定比 G1 好ZGC 的适用场景是大堆64G 低分配率。在中小堆8G下G1 更稳定RT 敏感服务重点关注慢请求分布即使平均 RT 正常P99/P999 的抖动可能触发上游超时。GC 停顿是 P99 抖动的常见来源GC 选型直接影响 P99 稳定性显式比默认更安全所有 JVM 参数尽量显式声明不要依赖默认值。默认值在不同 JDK 版本之间可能无声变化附完整命令清单# 查看进程和 GC 参数jps-lvm|greppay-service# 查看 GC 状态连续采样jstat-gcutilpid10008# 查看 GC 原因jstat-gccausepid10003# 查看线程状态jstackpid|grep-E(biz|GC|ZGC)|head-20# GC 日志 - Allocation Stall 分布grepAllocation Stallgc.log|head-12# Allocation Stall 时长 Top 5awk{print $3}gc.log|grep-oP\d\.\d|sort-n|tail-5# Allocation Stall 累计耗时awk{match($3, /([0-9]\.[0-9])ms/, a); if(a[1]) suma[1]; count} \ END {printf %d 次, 累计 %.2fs, 平均 %.2fms\n, count, sum/1000, sum/count}gc.log# 确认 JDK 默认 GCjava-XX:PrintCommandLineFlags-version|grepUse# 启动参数对比# ZGC: java -Xms2g -Xmx2g -XX:UseZGC -jar app.jar# G1: java -Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis20 \# -XX:G1HeapRegionSize4m -jar app.jar

相关新闻

2026/7/11 23:56:29

最近,看到最系统的Harness工程综述

Claude Code、Codex 等 Coding Agent 产品的成功,已经证明了 Harness 的重要性。Blog系统梳理 Harness Engineering 的设计模式、优化层级,以及从 ACE 到 MCE 再到 Meta-Harness 的演进脉络。 全文概览 一、Harness:比早期 Agent 框架多了一…

2026/7/11 23:51:29

OBS多平台直播插件终极指南:5分钟解决你的直播痛点

OBS多平台直播插件终极指南:5分钟解决你的直播痛点 【免费下载链接】obs-multi-rtmp OBS複数サイト同時配信プラグイン 项目地址: https://gitcode.com/gh_mirrors/ob/obs-multi-rtmp 还在为每个直播平台单独配置OBS而烦恼吗?obs-multi-rtmp这款免…

2026/7/12 1:07:01

【苍穹外卖笔记】第四天内容

一、课程内容预览 自主完成套餐相关全部接口开发修复根据分类id查询菜品的Bug(额外) 二、笔记 第四天的内容没有视频资源,需要我们使用之前学习到的知识自主地完成套餐相关全部接口的开发,这部分需要各位手操完成,只是…

2026/7/12 1:07:01

Vulkan进阶系列20 - Vulkan 1.0 API 一览

分类 API 作用 Physical Device 查询 vkEnumeratePhysicalDevices 枚举系统里的 GPU vkGetPhysicalDeviceFeatures 查询 GPU 支持哪些功能(几何着色器、各向异性过滤等) vkGetPhysicalDeviceProperties 查询 GPU 基本信息(型号、版本、限制参数) vkGetPhysicalDeviceFormat…

2026/7/12 1:07:01

AI论文写作工具哪家强?多款主流平台深度对比与评测

一、毕业党的论文困境:AI论文工具是解药还是鸡肋? 每年毕业季,无数毕业生都在为“写不完的论文”头疼——选题换了又换,大纲改了又改,好不容易憋出正文,一查重30%以上,参考文献还是AI论文工具瞎…

2026/7/12 1:02:01

Gitee 代码提交贡献度无绿点:3 种常见原因排查与修复方案

Gitee 代码提交贡献度无绿点:3 种常见原因排查与修复方案当你满怀期待地在 Gitee 上提交了代码,却发现个人主页的贡献度图表上那个代表活跃的小绿点迟迟不肯出现,这种失落感就像精心准备的礼物被对方忽略了一样。作为开发者,我们不…

2026/7/12 0:01:29

3步解锁音乐自由:ncmdumpGUI终极NCM文件解密转换指南

3步解锁音乐自由:ncmdumpGUI终极NCM文件解密转换指南 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 你是否曾在网易云音乐下载了心爱的歌曲&#…

2026/7/12 0:01:29

CANoe 19 SP3 配置 GB/T 27930-2023 A类系统:3步搭建BMS仿真测试环境

CANoe 19 SP3 配置 GB/T 27930-2023 A类系统:3步搭建BMS仿真测试环境随着新能源汽车行业的快速发展,充电通信协议的标准化和测试验证变得尤为重要。GB/T 27930-2023作为中国智能充电协议的最新版本,对充电机与电动汽车之间的通信提出了更严格…

2026/7/12 0:01:29

3步搞定RTL8852BE驱动:从零开始配置Wi-Fi 6网卡

3步搞定RTL8852BE驱动:从零开始配置Wi-Fi 6网卡 【免费下载链接】rtl8852be Realtek Linux WLAN Driver for RTL8852BE 项目地址: https://gitcode.com/gh_mirrors/rt/rtl8852be 还在为Linux系统无法识别RTL8852BE Wi-Fi 6网卡而烦恼吗?&#x1f…

2026/7/12 0:01:29

3步解锁音乐自由:ncmdumpGUI终极NCM文件解密转换指南

3步解锁音乐自由:ncmdumpGUI终极NCM文件解密转换指南 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 你是否曾在网易云音乐下载了心爱的歌曲&#…

2026/7/12 0:01:29

CANoe 19 SP3 配置 GB/T 27930-2023 A类系统:3步搭建BMS仿真测试环境

CANoe 19 SP3 配置 GB/T 27930-2023 A类系统:3步搭建BMS仿真测试环境随着新能源汽车行业的快速发展,充电通信协议的标准化和测试验证变得尤为重要。GB/T 27930-2023作为中国智能充电协议的最新版本,对充电机与电动汽车之间的通信提出了更严格…

2026/7/12 0:01:29

3步搞定RTL8852BE驱动:从零开始配置Wi-Fi 6网卡

3步搞定RTL8852BE驱动:从零开始配置Wi-Fi 6网卡 【免费下载链接】rtl8852be Realtek Linux WLAN Driver for RTL8852BE 项目地址: https://gitcode.com/gh_mirrors/rt/rtl8852be 还在为Linux系统无法识别RTL8852BE Wi-Fi 6网卡而烦恼吗?&#x1f…

2026/7/11 8:37:53

3个高效策略:快速掌握Axure中文界面配置

3个高效策略:快速掌握Axure中文界面配置 【免费下载链接】axure-cn Chinese language file for Axure RP. Axure RP 简体中文语言包。支持 Axure 11、10、9。不定期更新。 项目地址: https://gitcode.com/gh_mirrors/ax/axure-cn 还在为Axure RP的英文界面感…