频繁触发full gc的根本原因是老年代内存失衡,需通过gc日志(查cause、old usage变化、ygc/fgc比)、堆快照(mat分析histogram与leak suspects)及运行态验证(arthas查静态集合、threadlocal、资源泄漏)定位是内存泄漏、对象晋升异常或配置负载不匹配。

频繁触发 Full GC 通常不是孤立现象,而是内存使用失衡的外在表现。真正要解决的,不是“怎么让 GC 少一点”,而是“为什么老年代总填不满又清不净”。关键在于区分是内存泄漏、对象晋升异常,还是配置与负载不匹配。
看 GC 日志,先锁定触发类型
打开 GC 日志(JDK8 用 -XX:+PrintGCDetails,JDK11+ 用 -Xlog:gc*),重点抓三类线索:
-
GC Cause 字段:出现
Allocation Failure说明老年代真没空间了;Promotion Failed表示新生代对象想晋升但老年代碎片太多或预留不足;System.gc()则是代码里写了Runtime.getRuntime().gc()或框架隐式调用(如某些序列化库) - 老年代使用率变化:每次 Full GC 后,Old Usage 只下降 2%–5%,且持续爬升到 90%+,大概率存在泄漏;若 GC 后骤降 60% 以上,再快速回升,更可能是短时大对象或突发流量导致
-
YGC/FGC 比例:用
jstat -gc <pid></pid>查看,健康比值应 > 10:1;若接近 1:1 甚至倒挂,说明大量对象活过多次 Minor GC,直接扎进老年代
查堆快照,聚焦存活对象源头
在 Full GC 高发时段,用 jmap -dump:format=b,file=heap.hprof <pid></pid> 抓取堆镜像,用 MAT 打开后重点关注:
-
Histogram 视图:按 “Shallow Heap” 排序,找实例数最多或单个对象占用内存最大的类(比如
byte[]、HashMap$Node、自定义缓存类) -
Leak Suspects 报告:MAT 自动生成的疑似泄漏路径,优先验证那些被
static、ThreadLocal、监听器或未关闭资源强引用的对象 - Path to GC Roots(排除弱引用):对可疑对象右键 → “Show Objects by Class” → 再选 “Merge Shortest Paths to GC Roots”,确认它为何无法被回收
验运行态,排查典型泄漏模式
有些问题日志和堆 dump 看不出,必须结合运行中行为验证:
-
静态集合膨胀:用 Arthas 执行
watch com.xxx.CacheService put '{params,returnObj}' -n 10,看是否持续写入未清理的 key-value -
ThreadLocal 泄漏:检查线程池中是否复用线程但没调用
threadLocal.remove(),尤其注意异步逻辑或过滤器中 set 后忘记 clear -
连接/流未释放:搜索代码中
new Socket()、Connection、InputStream是否都在 finally 块或 try-with-resources 中 close -
大对象直入老年代:若应用频繁创建 > 1MB 的 byte[] 或 ArrayList,检查是否设置了
-XX:PretenureSizeThreshold,或考虑改用对象池复用
调参数前,先确认是不是真需要调
盲目增大堆或调整比例常掩盖问题,反而延缓定位:
- 如果老年代增长斜率稳定上升,调大
-Xmx只是推迟爆炸时间,泄漏仍在继续 - 若
jstat -gccapacity <pid></pid>显示 S0C/S1C 长期低于 1MB,说明 Survivor 区太小,对象一“活”就晋升,应调大-XX:SurvivorRatio或启用-XX:+AlwaysTenure(谨慎) - Metaspace 持续上涨?不是简单加
-XX:MaxMetaspaceSize,先用jcmd <pid> VM.native_memory summary</pid>查动态类加载来源(如 Spring Boot DevTools、OSGi、字节码增强框架)











