虚拟机Full GC频繁,本质上是老年代空间被快速占满或堆内存分配失衡,别急着加内存,先打开GC日志定位是晋升过快、内存泄漏还是配置不当。
第一步:确保JVM输出GC日志,没有日志一切都是瞎猜
很多线上事故排查时才发现,JVM根本没开GC日志,这就像开车不看仪表盘,等发动机红灯亮了才去猜哪里出了问题,对于Java虚拟机查看gc日志的需求,最直接的操作是给JVM加上以下参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
-Xloggc:/data/logs/gc-$(date +%Y%m%d).log
JDK 8及以下版本用上面这组,JDK 11+建议改用统一的日志风格:
-Xlog:gc:/data/logs/gc.log:time,uptime,level,tags
日志文件建议按天切割,配合logrotate使用,否则日志膨胀会撑爆磁盘。
linux查看gc日志命令:先学会这4个快速定位指令
拿到gc.log文件以后,别用文本编辑器直接拉到底部,文件可能几百MB,直接在Linux服务器上执行以下命令,效率高得多:
- 查看Full GC出现次数:
grep "Full GC" gc.log | wc -l - 查看每次Full GC的停顿时间:
grep "Full GC" gc.log | awk '{print $4}' | sort -n | tail -20 - 查看GC前后的堆空间变化:
grep "Full GC" gc.log | tail -50 - 连续导出GC摘要:把GC日志和JVM线程快照放一起看,命令组合为
jstat -gcutil <pid> 5000,每5秒输出一次堆内存使用率
日志中关键字段的含义大致是:GC前的数字是回收前内存占用,->后是回收后占用,括号内是该区域总容量,比如[Full GC 680M->120M(1024M), 0.8234567 secs],意思是这次Full GC把老年代从680M压到了120M,耗时约0.82秒。
这些日志特征说明Full GC已经开始频繁敲门
Full GC频繁不是瞬间发生的,日志里通常有迹可循,观察gc.log时,重点看以下模式:
- 连续多次Full GC间隔小于几秒,比如每分钟出现5次以上,说明老年代几乎每次回收后又被迅速填满
- 每次Full GC后堆占用率下降幅度极小,比如回收前900M回收后800M,说明垃圾对象占比很低,大量对象是存活的
- Full GC前伴随大量Minor GC,且Minor GC后晋升到老年代对象的大小持续增长,说明对象晋升年龄和速率异常
- 日志中出现
Metadata GC Threshold或CMS permanent generation字样,说明元数据区或永久代空间不足,也会触发Full GC
第二步:用jstat和jmap快速定位元凶
日志告诉我们“发生了什么”,但没告诉我们“谁干的”,跟进Full GC原因分析,下一刀切在堆内存快照上。
jstat看趋势,判断是内存泄漏还是内存分配过大
对运行中的Java进程执行:
jstat -gcutil <pid> 1000 10
输出结果中重点看O列(老年代使用率),如果老年代使用率从20%一路匀速爬到95%,然后触发Full GC降回30%,再过一段时间又匀速爬升,这个循环周期越来越短,基本可以判定存在对象渐进式泄漏,如果老年代使用率呈现锯齿状波动,但始终在60%-90%区间震荡,通常是堆大小设置不合理或业务峰值流量分配不均导致。

jmap + MAT分析堆快照,找出占用内存的类
Full GC频繁时,堆转储文件相当大,直接在生产环境导出有风险,建议在Full GC发生后的低峰期执行:
jmap -dump:live,format=b,file=/data/logs/heap.hprof <pid>
拿到快照后用Eclipse MAT或VisualVM分析,优先查看支配树(Dominator Tree)视图,操作路径是:MAT打开快照 -> 选择Dominator Tree -> 按Retained Heap排序,注意力放在那些占用堆内存80%以上的对象上,通常问题出在几个方向:
- 缓存类对象:本地缓存未设置过期时间,或者缓存淘汰策略不生效,比如用HashMap做缓存却忘了清理
- 线程局部变量:ThreadLocal持有的对象未调用
remove(),在高并发场景下每个线程都积累一份数据 - 数据库连接和会话:连接池泄漏,大量Connection对象未被归还,常见于JDBC代码里没有finally块关闭连接
- 集合类容器:往ArrayList或HashMap里无限量添加业务数据,比如批量查询结果一次性装载到内存
第三步:Full GC频繁触发原因逐个排查,按概率排序
根据行业内线上事故统计,Full GC频繁最常见的原因存在明显的概率差异,以下按多数情况下的发生频率从高到低排列:
| 原因类型 | 典型场景 | 排查方法 |
|---|---|---|
| 堆大小配置过小 | 业务流量涨了但JVM参数没变 | 查看GC日志中堆总容量,对比物理内存 |
| 大对象直接进入老年代 | 一次性查询上万条记录,或创建超大数组 | 查看-XX:PretenureSizeThreshold参数配置 |
| 晋升阈值设置不当 | Minor GC频繁且晋升率远超预期 | 查看-XX:MaxTenuringThreshold参数 |
| 内存泄漏 | 缓存、连接、ThreadLocal未释放 | 堆快照对比,连续两次dump比对 |
| CMS或G1回收器参数异常 | 并发模式失败,或Mixed GC周期过长 | 查看日志中concurrent mode failure关键字 |
老年代空间本身就不够,怎么判断和调整
GC日志中老年代容量和每次Full GC后的使用率,是最直接的判断依据,假设日志显示老年代容量为2G,Full GC后老年代使用率仍然在70%以上,说明活着的老对象就在1.4G左右,这时候调大堆大小是合理的,具体到参数调整,行业共识认为:
- 堆大小调整:在启动参数中修改
-Xms和-Xmx,两者建议设为相同值,避免运行期堆自动扩容带来的性能抖动 - 设置合理的年轻代比例:
-XX:NewRatio=2表示老年代与年轻代比例为2比1,如果对象生命周期短但Minor GC压力大,可调大年轻代比例 - 动态晋升检查:JDK 8默认开启
-XX:+UseAdaptiveSizePolicy,这会动态调整各代大小,如果日志显示每次Minor GC都触发晋升,可以考虑关闭自适应策略,手动指定
-XX:SurvivorRatio=8
代码里疯狂创建大对象,如何从GC日志里识别出来
大对象问题是Full GC频繁的重灾区,当Java虚拟机查看gc日志时,注意Desired survivor size和n objects promoted这两组字段,Minor GC日志中如果出现大量对象被晋升(promoted),且这些对象占用空间超过老年代空闲空间的一半,那很可能是大对象在作祟。
识别出大对象场景后,代码层面的修复思路是:
- 将一次性负载万条数据的查询改为分批加载,每批1000条处理完后释放引用
- 使用
WeakReference或SoftReference包装缓存值,让GC在内存紧张时优先回收缓存对象 - 对于网络传输或文件读取的byte数组,使用
ByteArrayOutputStream时指定初始容量,避免反复扩容 - 批量处理任务时,每处理完一批就显式置空集合引用,防止迭代变量被list保留
第四步:G1和CMS收集器的Full GC特殊场景处理
如果JVM使用G1垃圾收集器,Full GC的原因分析与CMS略有不同,G1的Full GC通常是由Humongous Allocation(巨型对象分配)或Mixed GC回收不及时导致。
G1日志中特有的三个关键字
humongous allocation:当对象大小超过Region容量的50%,G1会把它直接分配到老年代Region,连续分配过多巨型对象会导致老年代快速被占满,对G1来说,掌握Full GC频繁怎么排查的核心,就是先检查巨型对象是否泛滥,启动参数中显式设置-XX:G1HeapRegionSize=16m,可以改变Region大小,使更多对象不被判定为巨型对象concurrent mode failure:后台并发标记线程还没执行完,老年代就被新对象占满,被迫转为STW的Full GC,可以针对此提升并发标记线程数-XX:ConcGCThreads或加大堆空间to-space exhausted:G1复制回收时目标Region空间不足,排查时关注-XX:G1ReservePercent参数,默认值是10,调大到15或20可以预留更多晋升空间
什么时候应该换回收器,什么时候不该换
CMS已经在JDK 14中被移除,但很多老系统还在用,如果日志反复出现concurrent mode failure且每次停顿都超过1秒,与其花大精力调CMS参数,不如直接升级到G1,但如果应用堆内存始终在4G以下,且延迟要求极高,换G1不一定比Parallel Scavenge更优,业内专家指出,堆内存小于4G的应用,用Parallel Scavenge + Parallel Old往往比G1表现更稳定。
第五步:编制一套可复用的Full GC排查清单
前面四步讲了很多具体操作,最后汇总成一套可以直接照着执行的排查清单,出现Full GC频繁问题时,按顺序逐个确认:
- 第一步:确认GC日志是否开启,若未开启则立即加上参数并重启JVM
- 第二步:统计Full GC频率和间隔,用
grep和awk快速提取日志中的时间戳和堆用量 - 第三步:用
jstat -gcutil观察老年代增长率,区分内存泄漏和分配压力 - 第四步:用
jmap导出堆快照,用MAT分析对象引用链,定位到具体业务代码 - 第五步:检查JVM参数是否合理,包括堆大小、晋升阈值、垃圾回收器配置
- 第六步:如果以上步骤均未发现明显异常,检查操作系统层面,比如cgroup内存限制与JVM堆大小不匹配

这个清单不是一次性的,建议每季度对线上主要服务做一次GC日志体检,主动发现问题比事故后进行Full GC原因分析要减少很多痛苦。
Full GC日志分析常见误解与正解
- 误解一:Full GC次数多就一定代表内存泄漏,正解:有些Full GC完全正常,比如堆空间不足时GC回收了90%以上的内存,说明程序运行良好,只是堆给小了
- 误解二:只要配置了G1就不需要关心Full GC,正解:G1有自己的触发机制,也会产生STW停顿,仍需要关注日志中的
humongous allocation和to-space exhausted - 误解三:加大堆内存就能解决Full GC频繁,正解:堆过大反而导致GC时间变长,且如果存在内存泄漏,堆再大也扛不住,据公开的技术分享数据,大多数Full GC问题通过调整参数和优化代码可以解决,真正需要扩容堆的场景不到三分之一
Full GC日志分析真的比想象中更依赖工具吗
不少人习惯手动打开gc.log一行行读,既慢又容易遗漏,当前行业里常用的分析工具有GCeasy、GCViewer、GCEasy在线分析平台,以GCeasy为例,只需把gc.log拖拽到网页上,几秒钟就能输出Full GC次数统计、停顿时间分布、最大停顿点等信息,不过在线分析涉及数据隐私问题,内网环境建议部署GCViewer,开源免费,支持本地运行,GCViewer运行方式和用法如下:
java -jar gcviewer-1.36.jar /data/logs/gc.log
界面中红色竖线代表Full GC,左侧Y轴是堆使用率,如果红色竖线密集且在堆使用率飙高后出现,确实说明老年代空间不足;如果红色竖线出现在堆使用率较低的阶段,则指向JVM元空间或本地内存问题。
常见问题解答
Full GC多久一次算频繁?
没有绝对标准,取决于业务和延迟要求,Full GC停顿超过1秒且每5分钟发生一次,对绝大多数业务已经算是频繁,如果业务对延迟敏感,哪怕一天一次超1秒的Full GC也需要关注,最低标准是:Full GC频率不随业务流量波动而急剧上升。
加了-Xmx8g为什么Full GC还是很频繁?
堆大小只是容器,不是免死金牌,堆转储后如果发现大量存活对象占用了6G以上,那说明程序本身持有的长生命周期对象过多,这类情况下重点检查静态集合类、缓存框架和数据库连接池是否合理配置,另外确认-Xmx确实生效,使用jinfo -flag MaxHeapSize <pid>验证。
CMS和G1的Full GC日志格式有什么区别?
CMS日志中Full GC通常包含[Full GC (System.gc())或[Full GC (Ergonomics)前缀,G1日志中则是[Full GC (Allocation Failure),CMS模式下Full GC是单线程串行回收,停顿时间线性增长;G1的Full GC虽然也使用串行回收,但G1会尝试在Full GC前执行更高效的Mixed GC,实际效果更优,排查思路一致,只是日志里搜索的关键字不同。