GC 频繁排查
2023年春节假期后第一天上班,某社交平台的客服系统收到了大量投诉:接口响应时间从正常的50ms飙升到500ms,用户体验严重下降。
技术团队排查后发现:服务每天凌晨3点会执行一次全量数据同步,同步过程中会产生大量临时对象。这些对象触发了频繁的Minor GC,但因为堆内存设置偏小(只有4G),Minor GC回收不充分,导致每隔10分钟就触发一次Full GC。
Full GC停顿时间约500ms,累计影响了约10万次请求。
这是一个典型的"GC参数设置不合理 + 业务逻辑问题"导致的性能问题。
【面试官手记】
GC频繁是Java服务最常见的性能问题之一。我面试过的候选人里,能说出"G1和CMS区别"的不超过40%,能说出"怎么通过GC日志定位问题"的不超过20%。GC问题的关键不是背参数,而是理解GC的原理和调优思路。
一、GC频繁的常见原因 🔴
1.1 五大原因
GC频繁的五大原因:
1. 内存设置不合理
- 堆内存太小,对象无法在Young区分配
- Survivor区太小,对象直接进入Old区
- 典型症状:Full GC频繁
2. 对象分配过快
- 瞬时创建大量对象
- 典型场景:批处理、定时任务、数据同步
- 典型症状:Minor GC频繁,耗时增加
3. 内存泄漏
- 无用对象无法被回收
- 导致Old区逐渐填满
- 典型症状:Old区持续增长,Full GC后不下降
4. GC参数不合理
- 选择错误的GC算法
- 参数设置不当
- 典型症状:GC停顿时间过长
5. 大对象直接进入Old区
- 大对象分配失败进入Old区
- 典型场景:大数组、大集合
- 典型症状:Old区突然增长
1.2 GC监控指标
GC日志关键指标:
YoungGC:
- 频率:正常约几秒到几十秒一次
- 耗时:正常 < 50ms
- 回收量:应该大于Eden区大小
FullGC:
- 频率:正常约几分钟到几小时一次
- 耗时:CMS约200-500ms,G1约100-300ms
- 回收量:应该接近Old区使用量
停顿时间目标:
- P99 < 200ms
- 最大停顿 < 500ms
- 用户无感知 < 50ms
1.3 面试追问
面试官:GC频繁怎么排查?
候选人:首先看GC日志,分析是Minor GC还是Full GC频繁。
面试官:Minor GC频繁怎么处理?
候选人:Minor GC频繁说明对象创建太快,可能是Young区太小或者Eden区太小。需要调整 -Xmn 和 Survivor区比例。
面试官:Full GC频繁怎么处理?
候选人:先看是老年代满了还是MetaSpace满了。老年代满了要排查内存泄漏;MetaSpace满了要排查类加载问题。
【面试官心理】
GC调优的追问通常很深入。能回答出"Minor GC频繁调Young区"的候选人,说明知道Young/Old的分工;能回答出"Full GC看Old区还是MetaSpace"的候选人,说明知道GC分类排查的思路。
二、排查流程 🔴
2.1 第一步:开启GC日志
# JVM参数开启GC日志
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintHeapAtGC \
-XX:+PrintTenuringDistribution \
-Xloggc:/path/to/gc.log \
-XX:NumberOfGCLogFiles=5 \
-XX:GCLogFileSize=10M
2.2 第二步:分析GC日志
# 实时分析GC日志
tail -f gc.log | grep -E "(GC|Allocation Failure|Full GC)"
# GC统计分析
cat gc.log | grep -E "Full GC" | wc -l # Full GC次数
cat gc.log | grep -E "Full GC" | awk '{sum+=$NF} END {print sum}' # Full GC总耗时
2.3 GC日志解读
GC日志示例(CMS):
[GC (Allocation Failure) [ParNew: 1234567K->12345K(1382400K), 0.0456789 secs] 2345678K->1123456K(3456000K), 0.0458901 secs] [Times: user=0.12 sys=0.03, real=0.05 secs]
GC日志解读:
- Allocation Failure:分配失败,触发GC
- ParNew:ParNew垃圾收集器(Young区)
- 1234567K->12345K:GC前->GC后(Young区)
- 1382400K:Young区总大小
- 0.0456789 secs:GC耗时
- 2345678K->1123456K:GC前->GC后(堆内存)
- 3456000K:堆总大小
Full GC日志:
[Full GC (Allocation Failure) [CMS: 1234567K->12345K(3456000K), 2.345678 secs] 2345678K->1123456K(3456000K), [Metaspace: 123456K->12345K]
2.4 在线分析工具
GC日志分析网站:
1. GCEasy:https://gceasy.io
- 上传GC日志,自动分析
- 生成报告,标注问题
2. GCViewer
- 本地工具
- 离线分析
3. Google Garbage Cat
- 开源工具
- 支持实时监控
三、常见场景与优化 🟡
3.1 场景一:Minor GC频繁
原因:对象分配速率过高,Young区太小
症状:
- YoungGC频率 < 1秒
- YoungGC后 Eden区几乎被填满
优化方案:
1. 增加Young区大小
-Xmn1024m -XX:NewRatio=2
2. 调整Survivor区比例
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
3. 优化代码
- 减少临时对象创建
- 对象复用
3.2 场景二:Full GC频繁
原因:Old区对象过多
症状:
- Full GC频率 < 5分钟
- Full GC后Old区下降不明显
优化方案:
1. 增加堆内存
-Xmx8g -Xms8g
2. 调整Old区比例
-XX:NewRatio=2 # Old:Young = 2:1
3. 调整CMS触发时机
-XX:CMSInitiatingOccupancyFraction=70 # 70%时开始回收
3.3 场景三:CMS GC中断
原因:CMS并发模式失败
症状:
- 日志出现 "concurrent mode failure"
- 触发一次STW的Full GC
优化方案:
1. 增加堆内存
2. 提前启动CMS
-XX:CMSInitiatingOccupancyFraction=60
3. 使用G1代替CMS
四、GC调优参数 🟡
4.1 堆内存设置
# 通用推荐配置
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小
-Xmn2g # Young区大小
-XX:NewRatio=1 # Old:Young = 1:1
# G1推荐配置
-Xms4g
-Xmx4g
-XX:MaxGCPauseMillis=200 # 最大GC停顿目标
-XX:+UseG1GC
-XX:G1HeapRegionSize=4m
4.2 G1调优
G1关键参数:
- MaxGCPauseMillis:最大停顿目标(默认200ms)
- G1HeapRegionSize:Region大小(1-32MB)
- InitiatingHeapOccupancyPercent:开始Mixed GC的阈值(默认45%)
G1调优思路:
1. 设置合理的停顿目标
-XX:MaxGCPauseMillis=200
2. 观察停顿时间分布
-XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime
3. 调整Region大小
- 大数据量:Region=8m
- 小数据量:Region=1m
五、生产避坑 🟡
5.1 GC排查的五大坑
坑1:只看GC次数不看耗时
问题:GC次数不多,但每次耗时很长
场景:Full GC耗时2秒
解决方案:
- 不仅看GC次数,还要看GC耗时
- 设置停顿时间告警
坑2:只看Full GC忽略Minor GC
问题:Minor GC耗时累积也很可观
场景:Minor GC每次50ms,每秒10次 = 500ms/秒
解决方案:
- 统计所有GC的总耗时
- GC总耗时/运行时间 < 5%
坑3:参数调优盲目
问题:网上找了参数直接套用
场景:4核8G机器用了16核的参数
解决方案:
- 根据机器配置调整参数
- 通过压测验证效果
坑4:不停重启掩盖问题
问题:重启后正常,过几天又出问题
场景:内存泄漏,重启只治标不治本
解决方案:
- 找到根本原因
- 修复代码问题
坑5:忽略了Metaspace
问题:只看Heap,忽略Metaspace
场景:大量动态类加载
JDK 8+解决方案:
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
5.2 GC调优目标
GC调优的三个层次:
层次1:没有Full GC
- 优化目标:避免Full GC
- 方法:合理设置Old区大小
层次2:GC停顿 < 200ms
- 优化目标:P99停顿 < 200ms
- 方法:选择合适的GC算法
层次3:GC停顿 < 50ms
- 优化目标:用户无感知
- 方法:极致的GC调优
建议:先达到层次1,再追求层次2,最后考虑层次3。
六、真实面试回放 🟡
面试官:G1和CMS的区别是什么?
候选人(小张):G1是CMS的升级版,主要区别:
-
内存布局不同。CMS是 Old + Young + Survivor,G1是把堆分成多个大小相等的Region。
-
回收方式不同。CMS是标记-清除,会产生内存碎片;G1是标记-整理,没有内存碎片。
-
停顿控制不同。CMS无法控制停顿时间;G1可以设置 MaxGCPauseMillis。
面试官:什么场景适合用G1?
小张:两个场景:
一是堆内存 > 4G。CMS在大堆下性能下降明显,G1更稳定。
二是需要控制停顿时间。G1可以设置停顿目标,CMS只能被动回收。
面试官:什么场景用CMS不用G1?
小张:大概两个场景:
一是JDK版本低。JDK 8默认是Parallel GC,如果要用CMS需要显式开启;G1需要JDK 9+。
二是旧系统已经稳定。CMS虽然有内存碎片,但已经优化好了,没必要换成G1。
【面试官手记】
小张这场面试的亮点:
-
知道G1和CMS的内存布局区别:连续 vs 分Region
-
知道G1和CMS的算法区别:标记-整理 vs 标记-清除
-
知道G1的停顿控制能力
-
知道选型依据:堆大小、JDK版本
GC调优是P6工程师必备技能,能回答出G1和CMS区别的候选人,说明理解了GC原理。
追问方向:会问"G1的Mixed GC是什么"和"怎么设置G1的参数",这些是更深入的追问。
GC频繁排查的核心是看日志 → 定原因 → 调参数。记住三个要点:
- Minor GC频繁:调Young区大小、减对象创建
- Full GC频繁:调Old区大小、排查内存泄漏
- CMS vs G1:大堆用G1,小堆用CMS
GC调优的目标:没有Full GC → 停顿 < 200ms → 停顿 < 50ms。