长时间 GC 停顿(Long GC Pauses)是 JVM 高可用系统中的“致命杀手”,尤其在大堆、高并发、低延迟要求的场景下(如 Kafka、ZooKeeper、交易系统),可能导致服务超时、雪崩甚至宕机。
以下是 JVM 长时间 GC 停顿的深度定位与解决方案,涵盖三大主流 GC 策略(Parallel、CMS、G1)的典型问题、分析工具和优化策略。
⏳ JVM 常见问题:长时间 GC 停顿(Long GC Pauses)
GC 停顿(Stop-The-World, STW) 是指垃圾回收期间,所有应用线程被暂停的时间。
当 STW 超过几百毫秒甚至数秒时,就会严重影响服务响应能力。
一、根本原因分类
| GC 类型 | 典型停顿场景 | 根本原因 |
|---|---|---|
| Parallel GC | Full GC(Parallel Old) | 大堆 + 串行压缩 → 停顿可达数秒 |
| CMS | 重新标记(Remark)阶段 | 对象多、引用链复杂、Card Table 扫描慢 |
| G1 | Mixed GC 阶段 | Region 数多、大对象(Humongous Object)、Remembered Set 处理慢 |
| 通用 | Full GC(任何原因) | 内存泄漏、晋升失败、System.gc()、堆设置不合理 |
二、排查工具链
✅ 1. jstat -gc / jstat -gcutil:实时观察 GC 阶段耗时
jstat -gc <pid> 1000
输出示例:
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
512.0 512.0 0.0 512.0 4096.0 2345.6 8192.0 7800.0 1024 900 123 1.234 45 10.567 11.801
FGCT:Full GC 总耗时 → 若单次 > 1s,说明停顿严重- 结合时间间隔判断频率
🔍 更细粒度:需依赖 GC 日志
✅ 2. GC 日志分析(核心工具)
启用详细 GC 日志:
-Xloggc:/var/log/app/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintReferenceGC # 打印引用处理耗时
-XX:+PrintAdaptiveSizePolicy # 调试用
示例:CMS 重新标记阶段耗时长
2025-04-05T10:00:00.123+0800: 123.456: [GC remark 123.457: [Finalize Marking 0.234 sec]
[GC Ref Proc 0.123 sec] [Unloading 0.045 sec], 0.678 sec]
Finalize Marking:最终标记对象可达性GC Ref Proc:处理软/弱/虚引用Unloading:卸载类- 总耗时
0.678s→ 过长!
✅ 3. JFR(Java Flight Recorder)——生产级诊断神器
JFR 可以无侵入地记录 JVM 内部事件,包括 GC 各阶段耗时、线程暂停、内存分配 等。
使用步骤:
# 1. 启动 JFR 录制(生产推荐)
jcmd <pid> JFR.start name=long-pause duration=60s filename=/tmp/flight.jfr
# 2. 分析结果
jfr open /tmp/flight.jfr
或使用 JDK Mission Control (JMC) 图形化分析:
- 查看 “Garbage Collections” 标签页
- 定位哪次 GC 停顿最长
- 查看 “Pause Times” 按阶段分解(如 Remark、Evacuation)
✅ 优势:精准到毫秒级,支持生产环境低开销采样(默认 < 2%)
三、按 GC 类型分析长时间停顿
1. Parallel GC:大堆 + Parallel Old → Full GC 停顿过长
现象:
- Full GC 停顿时间随堆增大线性增长
- 日志中:
[Full GC (Ergonomics) [PSYoungGen: ...] [ParOldGen: ...] ..., 5.678 secs]
原因:
- Parallel Old 使用 单线程压缩老年代(默认),大堆下耗时极长
解决方案:
| 措施 | 说明 |
|---|---|
| ❌ 避免使用 Parallel GC 大堆 | 不适合 > 8GB 堆 |
| ✅ 改用 G1GC 或 ZGC | 支持大堆、低延迟 |
| 调整参数(临时缓解) |
-XX:+UseParallelOldGC # 启用并行老年代回收(默认开启)
-XX:ParallelGCThreads=8 # 增加并行线程数
2. CMS:重新标记(Remark)阶段耗时过长
现象:
- Remark 阶段耗时 > 500ms
- GC 日志中
Finalize Marking或GC Ref Proc时间长
原因:
- 老年代对象太多(> 100万)
- 引用链复杂(如缓存、图结构)
- Card Table 扫描慢(脏卡多)
- 软引用/弱引用过多(如 WeakHashMap、ThreadLocal)
解决方案:
| 措施 | 说明 |
|---|---|
| 减少老年代对象 | 优化缓存策略、避免内存泄漏 |
| 缩短引用链 | 避免深层嵌套对象图 |
| 减少软/弱引用 | 检查 WeakHashMap、ThreadLocal 使用 |
| 调优 CMS 参数 |
-XX:CMSScavengeBeforeRemark # Young GC 后再 remark,减少扫描量
-XX:+UseConcMarkSweepGC
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=4
| ✅ 终极方案 | 迁移到 G1GC 或 ZGC
⚠️ 注意:CMS 已在 JDK 14 被废弃,JDK 17+ 不再支持。
3. G1:Mixed GC 耗时超过 MaxGCPauseMillis
现象:
- Mixed GC 阶段持续进行,但每次耗时 > 设置的
MaxGCPauseMillis - 应用仍感觉卡顿
原因:
| 原因 | 说明 |
|---|---|
| Region 太多 | 堆太大,G1 需处理大量 Region |
| 大对象(Humongous Object) | > 50% Region Size 的对象,直接进老年代,难回收 |
| Remembered Set(RSet)处理慢 | 跨 Region 引用多,扫描耗时 |
| 并发标记线程不足 | -XX:ConcGCThreads 设置过小 |
| 晋升过快 | 新生代对象大量进入老年代 |
解决方案:
| 措施 | 参数示例 | 说明 |
|---|---|---|
| 控制停顿时长 | -XX:MaxGCPauseMillis=200 | 目标值,非保证 |
| 减少 Humongous 对象 | -XX:G1HeapRegionSize=16m | 配合对象大小调整 |
| 增加并发线程 | -XX:ConcGCThreads=4 | 提升并发标记速度 |
| 提前触发并发标记 | -XX:InitiatingHeapOccupancyPercent=45 | 默认 45%,可调低 |
| 监控 Mixed GC | GC 日志中查看 Mixed GC 阶段耗时 |
🔍 工具:使用 GCEasy 或 GCViewer 分析 G1 各阶段耗时。
四、通用优化策略
| 策略 | 说明 |
|---|---|
| 避免大堆 | 堆越大,GC 停顿越难控制。建议单机堆 ≤ 16GB |
| 使用低延迟 GC |
- G1GC:适合 4GB ~ 16GB,停顿 < 500ms
- ZGC(JDK 11+):支持 TB 级堆,停顿 < 10ms
- Shenandoah:类似 ZGC,Red Hat 主导
| 禁用显式 GC |-XX:+DisableExplicitGC防止System.gc()触发 Full GC |
| 监控与告警 | Prometheus + JMX Exporter + Grafana 监控GC pause time|
| 压测验证 | 模拟生产流量,观察 GC 表现 |
五、完整排查流程
[服务卡顿] → top 查看 CPU 是否被 GC 占用
↓
jstat -gc 查看 FGC 频率与耗时
↓
分析 GC 日志:定位哪次 GC 停顿最长
↓
使用 JFR 录制:查看各阶段耗时(Remark、Evacuation 等)
↓
判断 GC 类型问题:
├─ Parallel → 改用 G1/ZGC
├─ CMS → 优化 remark 或迁移
└─ G1 → 调参 + 避免大对象
↓
生成 Heap Dump → MAT 分析内存结构
↓
优化代码 + 调整 JVM → 验证效果
✅ 总结:长时间 GC 停顿处理口诀
📊 一查 jstat:看
FGCT是否过高
📄 二析 GC log:找哪一阶段耗时最长
🎥 三用 JFR:精准定位 STW 分布
🔍 四看对象:大对象?引用链长?
🛠️ 五调 GC:G1/ZGC 替代 Parallel/CMS
📈 六上监控:Prometheus + Grafana 实时告警
📌 最终建议:
长时间 GC 停顿是“慢性病”,往往在流量增长后突然爆发。
最佳实践:
- 所有生产应用 必须开启 GC 日志
- 大堆服务(> 8GB)优先使用 ZGC 或 G1GC
- 定期使用 JFR + MAT 进行健康检查
- 建立 GC 停顿告警(如 > 1s 触发)
- 关键系统考虑 ZGC(JDK 11+)实现亚毫秒级停顿
通过这套方法论,可有效控制 GC 停顿,保障系统的高可用与低延迟响应。
11万+

被折叠的 条评论
为什么被折叠?



