本文目录导读:

分析并解决线上频繁的Full GC问题,是Java后端开发和运维中非常核心的技能,这通常意味着堆内存压力过大、对象分配速率过高或元空间/直接内存异常。
下面是一个结构化、可操作的排查与解决指南,从现象到根因,再到具体措施。
核心思路:从“症状”定位“病因”
不要一上来就改JVM参数,正确的流程是:确认现象 → 获取现场数据 → 分析日志与快照 → 定位代码问题 → 制定优化方案。
第一步:确认症状与影响
- 监控告警:CPU飙升、接口超时、GC频率告警(如1小时内Full GC超过N次)。
- 服务日志:出现
java.lang.OutOfMemoryError、GC overhead limit exceeded。 - 用户反馈:页面加载缓慢、请求大量失败。
优先做:立即保留现场(导出GC日志、Heap Dump、线程栈),再进行重启或扩容。
第二步:获取关键现场数据
这是最关键的一步,数据决定了分析方向。
-
GC日志:最直接的证据。
- 如何开启:在JVM启动参数中加入(强烈建议线上生产环境始终开启):
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M
- 如何分析:使用工具(如
gceasy.io, fastthread.io 或GCViewer)。- 关注Full GC的频率(多久一次)、持续时间(STW停顿时长)。
- 关注GC前后的堆内存占用(Old Gen / Metaspace / Direct Buffer)。
- 如何开启:在JVM启动参数中加入(强烈建议线上生产环境始终开启):
-
堆转储文件 (Heap Dump):找到谁占用了内存。
- 如何获取:
- 自动:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof - 手动:
jmap -dump:live,format=b,file=heap.hprof <pid>
- 自动:
- 如何分析:使用
Eclipse MAT或JProfiler。- 重点:查看
Histogram(直方图) 中占用内存最大的对象,查看Dominator Tree(支配树) 找到GC Root(垃圾回收根对象)的路径。
- 重点:查看
- 如何获取:
-
JVM实时监控:看趋势。
- 命令:
jstat -gcutil <pid> <间隔毫秒> <次数> - 关注指标:
S0/S1:Survivor区使用率(波动剧烈)。E:Eden区使用率(增长过快表示对象创建快)。O:Old Gen使用率(持续增长且Full GC后降不下来,说明有老年代对象无法回收)。M:Metaspace使用率(类加载过多)。CCS:压缩类空间。YGC/YGCT:FGC/FGCT:次数和时间。
- 命令:
第三步:根据数据分析根因(典型场景)
场景1:老年代对象持续增长(Full GC后下降不明显)
- 现象:
jstat显示O区使用率在Full GC后依然很高(比如80%以上)。 - 根因:内存泄漏,对象被无意中持续引用,无法被GC回收。
- 排查:
- 用
jmap -histo:live <pid>对比Full GC前后的对象大小,如果总大小几乎不变,基本确认是泄漏。 - 用MAT分析Heap Dump,查看
Dominator Tree,找到最大的对象实例,然后看它的GC Root路径,常见元凶:- 静态集合类:
HashMap、ArrayList作为缓存/注册表,未释放。 - ThreadLocal:使用完未调用
remove(),导致线程池中的线程持有对象引用。 - 网络连接/IO流:未关闭的
Connection、InputStream。 - 自定义类加载器。
- 静态集合类:
- 用
场景2:对象直接进入老年代(Minor GC后进入老年代)
- 现象:Full GC频繁,但Minor GC后老年代使用率明显增长。
- 根因:对象过早晋升,大量存活对象在Minor GC时因Survivor区放不下或触发
Promotion Failure进入老年代。 - 排查:
- 检查
-XX:SurvivorRatio配置是否合理(如Eden:Survivor=8:1,Survivor区太小)。 - 检查
-XX:PretenureSizeThreshold(大对象直接进老年代阈值),是否设置过低。 - 检查代码中是否有大量生命周期短但体积大的对象(如一次性加载1MB的字符串)。
- 检查
场景3:元空间(Metaspace)或直接内存(Direct Memory)打爆
- 现象:
M区持续增长,或出现java.lang.OutOfMemoryError: Metaspace或Direct buffer memory。 - 根因:
- MetaSpace:动态生成大量类(CGLIB、JSP、Lambda表达式过多)。
- Direct Memory:Netty、NIO使用不当,未释放
ByteBuffer.allocateDirect()。
- 排查:
- MetaSpace:检查代码中是否有
Class.forName或热加载导致的类加载器泄漏。 - Direct Memory:使用
-XX:MaxDirectMemorySize限制大小,用btrace或Arthas监控Direct Buffer的分配。
- MetaSpace:检查代码中是否有
场景4:GC参数不合理
- 现象:GC日志显示
Full GC (Ergonomics)或Full GC (Allocation Failure)。 - 根因:JVM自动选择了不适合当前应用的垃圾收集器或参数。
- CMS GC 出现
Concurrent Mode Failure(并发模式失败)导致降级为Serial Old单线程Full GC。 - G1 GC 的
-XX:InitiatingHeapOccupancyPercent阈值过低,或-XX:G1HeapRegionSize太小。
- CMS GC 出现
- 排查:检查GC日志中的
Cause字段。CMS Initial Mark/Concurrent Mode Failure。
第四步:制定解决方案(按严重程度排序)
紧急止血(临时措施)
- 扩容:增加堆内存(
-Xms -Xmx),注意不要超过物理内存的70%,且预留系统内存。 - 切换GC:如果是在CMS上遇到
Concurrent Mode Failure,可临时切换为ParallelOld(吞吐量高但停顿长)或G1,但不要病急乱投医。 - 重启:如果确认是内存泄漏且无法快速修复,重启服务是最后的临时手段。
代码修复(根本原因)
- 修复内存泄漏:
- 确保所有
ThreadLocal使用后remove()。 - 为集合类缓存加上过期策略(如Guava Cache)或限制容量。
- 关闭资源(try-with-resources)。
- 确保所有
- 减少对象创建:
- 使用对象池(如数据库连接池、线程池)。
- 避免在循环中创建大量临时对象(如字符串拼接用
StringBuilder)。 - 使用
long代替Long等包装类,避免自动装箱。
- 控制大对象:
- 批量分页加载数据库数据,避免一次性加载10万条记录。
- 压缩或拆分大的网络传输对象。
JVM参数调优(锦上添花)
- 调大年轻代:
-XX:NewRatio=2或-Xmn,确保对象在Young GC时被回收,减少晋升。 - 调大Survivor区:
-XX:SurvivorRatio=6(Eden:Survivor=6:1:1),避免对象过早晋升。 - 优化GC触发阈值:
- CMS:
-XX:CMSInitiatingOccupancyFraction=70(默认值通常过高,设为60-70%可留出缓冲)。 - G1:
-XX:InitiatingHeapOccupancyPercent=45(默认45%,可适当调低以提前触发)。
- CMS:
- 选择更合适的GC:
- 低延迟要求:ZGC(JDK11+)、Shenandoah(JDK12+),但注意是实验性质的,需充分测试。
- 高吞吐量要求:Parallel Scavenge + Parallel Old(默认组合)。
- 平衡型:G1(JDK9+默认),推荐大多数线上服务使用。
第五步:验证与监控
- 灰度发布:先在测试环境或少量机器验证优化效果。
- 观察指标:
- Full GC频率是否降低(例如从每小时10次降到0次)。
- GC停顿时间是否在可接受范围内(如<200ms)。
- 应用接口响应时间(RT)是否恢复正常。
- 持续监控:配置好APM(如SkyWalking、Prometheus + Grafana)和JVM监控,设置告警阈值。
一个实际排查案例
假设线上告警:下午3点,CPU飙升到90%,接口超时。
- 保留现场:
jstack -l <pid> > thread.dump(看是否有死锁或阻塞);jmap -dump:live,format=b,file=heap.hprof <pid>。 - 查看GC日志:用GCeasy分析,发现每小时20次Full GC,每次停顿3秒,Old Gen占用了90%+。
- 分析Heap Dump:用MAT打开,
Dominator Tree显示java.util.HashMap占了80%内存,查看GC Root,发现是一个静态的ArrayList<Object>不断被添加数据(来自某个业务缓存且未清理)。 - 定位代码:
git blame找到提交人,发现在一个高并发调用的API里,使用了List.add()但忘记在finally块中清除。 - 解决方案:立即在代码中添加
list.clear()或改用WeakHashMap/Guava Cache并设置过期时间。 - 结果:修复后,Full GC降为每天0-1次,CPU恢复正常。
通过这个流程,你可以快速、精准地解决线上Full GC问题,避免盲目调参或重启。