Full GC频繁触发几乎都指向老年代空间不足或回收效率异常,排查的第一步是结合GC日志确认触发原因,再通过堆转储定位具体对象。GC日志记录每一次Full GC的前后堆占用、停顿时间和收集器行为,这些数据是判断根因最直接依据。
多数开发者遇到Full GC,第一反应是老年代满。实际上触发Full GC的场景有四种:老年代空间不足、元空间(Metaspace)扩容、代码显式调用System.gc()、以及晋升失败(Promotion Failed)。这四种场景在GC日志中的特征不同,处理方式也不同。
老年代空间不足是典型场景,通常表现为GC日志中老年代占用在Full GC后仍然大于90%。元空间扩容则与加载类数量直接相关,-XX:MaxMetaspaceSize设置过低会频繁触发Full GC。System.gc()调用可以通过-XX:+DisableExplicitGC禁掉。晋升失败发生在幸存区无法容纳晋升对象时,配合年轻代回收参数调整即可。
读取GC日志要抓住三个关键点:Full GC发生前的堆占用、回收以后的内存量、以及是否有“Promotion Failure”或“Metadata GC Threshold”关键字。这些信息直接告诉你内存瓶颈在哪。
以HotSpot的Parallel收集器为例,一次Full GC日志类似:
[Full GC (Ergonomics) 123456K->78901K(102400K), 0.1234567 secs]
[Tenured: 100000K->78901K(102400K), 0.0987654 secs] 1023456K->78901K(2048000K), 0.1234567 secs]这里“123456K->78901K”表明回收后老年代只剩78MB,占用不到10%,说明此次Full GC不是由老年代空间不足导致,更可能是堆外因素或元空间触发。如果回收后老年代仍然超过90%,则基本断定老年代空间不足。
另一个高价值字段是GC Cause(触发原因)。在G1收集器中,Full GC原因通常是“G1 Evacuation Pause”或“GCLocker Initiated GC”。前者代表对象分配速度超过G1回收集合的处理能力,后者代表JNI临界区中的引用更新,两者解法完全不同。
GC日志只能给出“哪里满”的结论,要回答“谁占满了”,必须分析堆转储(Heap Dump)。建议在Full GC发生频率达到每10秒一次时,立即用jmap dump下快照,同时观察业务逻辑中是否有集合类无限扩容。
使用MAT或jvisualvm打开堆转储,优先查看“Dominator Tree”和“Leak Suspects”报表。Dominator Tree会按照对象持有的保留内存大小排序,排在顶部的对象往往就是内存泄漏的持有者。一个常见的例子是ThreadLocalMap持有的ThreadLocal值未被移除,导致线程对象被缓存在Web容器线程池中,直至线程销毁才释放。
实际排查中,我遇到过一个订单服务每5分钟Full GC一次的案例,GC日志显示老年代回收后占用率从99%降到95%,始终无法下降。堆转储中发现一个静态HashMap存放了所有未关闭的历史连接对象,数量超过两百万。清除该集合后,Full GC间隔恢复到两小时以上。
不同收集器对Full GC的处理机制不一样,判断标准也不同。下表整理了四种常见收集器的Full GC特征,供排查时对照。
| 收集器 | Full GC触发阈值 | 回收行为 | 典型卡点场景 |
|---|---|---|---|
| Serial | 老年代或元空间满 | 单线程STW | 程序启动后快速打出大量对象 |
| Parallel | 老年代或元空间满 | 多线程STW | 大对象直接进入老年代 |
| CMS | 并发失败或老年代碎片化 | 退化为串行STW | 碎片导致并发无法回收 |
| G1 | 巨型对象分配失败 | 回收集合失败降级 | 分区内部对象过多 |
G1收集器在出现“Humongous Allocation”失败时触发Full GC,这种对象占分区大小的一半以上。如果业务中存在大数组或大缓存,就需要给G1单独设置-XX:G1HeapRegionSize,避免巨型对象跨区分配。
定位到具体原因后,调优的核心是让对象尽量在年轻代被回收,减少晋升到老年代的数量。三个参数需要重点关注:-XX:NewRatio(老年代与年轻代比例)、-XX:MaxTenuringThreshold(晋升年龄阈值)、-XX:SurvivorRatio(幸存区比例)。
对于无明显泄漏但有大量短生命周期对象的服务,把MaxTenuringThreshold从15降低到6,能让临时对象更早被年轻代回收。对于存在周期性大批量查询的服务,用-XX:MaxGCPauseMillis设定停顿目标,G1会根据目标动态调整分区收集策略。
还有一个经常被忽略的参数是-XX:+UseGCOverheadLimit,它会在GC时间超过98%时抛出OutOfMemoryError来提前终止应用,而不是长时间卡顿。在排查过程中不要盲改参数,每次只改动一个,用GC日志的前后数据对比验证。
Full GC的根因分析遵循“日志定方向,堆转储定对象,参数改行为”的三步法。记住,GC日志中回收后老年代占用率高于90%必然是空间分配问题,低于90%则要检查元空间和外部调用。用数据驱动而不是猜测,才是有效收敛Full GC频率的关键。
评论
加载中...