FullGC深度解析——当老年代也撑不住的时候前言在前面的系列文章中我们详细剖析了MinorGC的完整流程、动态年龄判定、空间分配担保等机制。但GC的世界里还有一个重量级角色——FullGC。如果说MinorGC是日常的小扫除那FullGC就是大扫除。它耗时更长、影响更大是JVM性能调优的重点关注对象。今天我们就来深入剖析FullGC的触发条件、执行过程、以及如何避免它成为性能瓶颈。一、FullGC是什么1.1 定义FullGCFull Garbage Collection是对整个堆内存新生代 老年代进行的一次完整垃圾回收。与MinorGC的区别对比维度MinorGCFullGC回收范围仅新生代Eden Survivor整个堆新生代 老年代触发频率频繁秒级/分钟级较少小时级/天级耗时短几毫秒到几十毫秒长几百毫秒到几秒STW时间短长影响较小可能造成应用卡顿1.2 FullGC的代价// FullGC期间的日志示例2024-01-01T10:00:00.1230800:[FullGC(System.gc())[PSYoungGen:51200K-0K(58880K)][ParOldGen:102400K-51200K(204800K)]153600K-51200K(263680K),[Metaspace:10240K-10240K(106496K)],0.5234567secs]关键信息耗时0.5秒远大于MinorGC的0.01秒应用在这0.5秒内完全卡顿Stop The World二、FullGC的触发条件FullGC不会无缘无故发生它有明确的触发条件。理解这些条件是优化JVM性能的第一步。2.1 显式调用System.gc()System.gc();// 建议JVM执行FullGC这是最直接的触发方式JVM不保证立即执行但通常会触发FullGC生产环境强烈建议禁用-XX:-DisableExplicitGC2.2 老年代空间不足这是最常见的FullGC触发原因场景1MinorGC时晋升失败 - MinorGC后存活对象太多 - 老年代也装不下这些对象 - 触发FullGC先清理老年代 场景2直接分配大对象 - 大对象如大数组直接进入老年代 - 老年代空间不足 - 触发FullGC 场景3老年代使用率达到阈值 - CMS回收器-XX:CMSInitiatingOccupancyFraction92 - 老年代占用超过92%时触发并发GC - 如果并发GC来不及退化为FullGC2.3 元空间Metaspace空间不足JDK 8以后永久代被元空间替代。如果元空间内存不足# 元空间满导致的FullGC[Full GC(Metadata GC Threshold)...]触发条件加载的类太多元空间不够用解决方案增大元空间-XX:MaxMetaspaceSize256m2.4 担保机制失败在MinorGC前的空间分配担保机制中// 担保机制判断if(老年代连续空间历史晋升平均值){// 直接触发FullGC不冒险进行MinorGCdo_full_gc();}2.5 显式调用Runtime.getRuntime().gc()Runtime.getRuntime().gc();// 和System.gc()效果相同2.6 JVM工具触发jmap -histo:live [pid] # 强制执行一次FullGCjcmd [pid] GC.run # 强制执行GC三、FullGC的执行过程以CMS为例CMSConcurrent Mark Sweep回收器的FullGC过程比较典型我们以它为例3.1 CMS的正常并发GC流程不是FullGC1. 初始标记STW短 标记GC Roots直接关联的对象 2. 并发标记并发 遍历对象图标记存活对象 3. 重新标记STW短 修正并发标记期间的变更 4. 并发清除并发 清除垃圾对象3.2 退化为FullGC的情况当CMS并发GC来不及清理或者老年代碎片太多时会退化为Serial Old FullGC[Full GC (Concurrent Mode Failure) ...]退化过程CMS并发标记期间用户线程不断产生新对象老年代剩余空间被迅速消耗CMS来不及完成并发清除暂停所有用户线程切换为Serial Old回收器使用标记-整理算法对整个堆进行FullGC为什么Serial Old更慢标记-整理算法需要移动对象单线程执行Serial需要遍历整个堆四、FullGC的执行过程以G1为例G1回收器的FullGC与CMS不同它有自己的机制4.1 G1的正常并发GC1. 初始标记STW 2. 并发标记 3. 最终标记STW 4. 筛选回收STW部分Region4.2 G1的FullGC当G1的并发回收来不及清理时也会退化为FullGC[Full GC (Allocation Failure) ...]G1的FullGC会暂停所有用户线程使用单线程的标记-整理算法对整个堆进行完整回收为什么G1也会有FullGC并发回收速度跟不上对象分配速度巨型对象Humongous Region过多GC停顿时间设置太短导致每次回收量不足五、FullGC的触发场景与排查5.1 场景一老年代空间不足现象[Full GC (Ergonomics) ... 51200K-51200K(102400K), 0.5 secs]注意回收后老年代仍然是满的51200K-51200K说明内存泄漏或内存不足。排查步骤使用jstat -gcutil [pid] 1000观察老年代占用率使用jmap -dump:live,formatb,fileheap.hprof [pid]导出堆转储用MAT分析查看大对象、引用链、可疑对象常见原因内存泄漏对象被意外持有无法回收缓存过大没有设置过期策略业务高峰期并发量超过预期5.2 场景二System.gc()显式调用现象[Full GC (System.gc()) ...]排查# 查看GC日志中是否有(System.gc())标记grepFull GC (System.gc)gc.log# 查找代码中的System.gc()调用grep-rSystem.gc()src/解决方案# JVM启动参数禁用显式GC-XX:DisableExplicitGC5.3 场景三元空间不足现象[Full GC (Metadata GC Threshold) ...] [Full GC (Metadata GC Threshold) ...] # 连续多次排查# 查看元空间使用情况jstat-gcutil[pid]1000# 关注MUMetaspace Used和MCMetaspace Capacity解决方案# 增大元空间-XX:MaxMetaspaceSize512m5.4 场景四担保机制失败现象[Full GC (Promotion Failed) ...]原因MinorGC时Survivor区装不下存活对象老年代也装不下晋升的对象解决方案增大新生代大小-Xmn增大Survivor区比例-XX:SurvivorRatio增大老年代大小-Xmx六、FullGC的优化策略6.1 减少FullGC频率优化方向具体措施原理增大堆内存-Xmx4g让老年代有更多空间调整新生代大小-Xmn1g减少对象晋升频率优化Survivor区-XX:SurvivorRatio8让对象在Survivor区多停留调整晋升阈值-XX:MaxTenuringThreshold15让对象晚点进老年代禁用显式GC-XX:DisableExplicitGC防止人为触发6.2 减少FullGC耗时优化方向具体措施原理选择合适的GCG1、ZGC低停顿减少STW时间优化GC线程数-XX:ParallelGCThreads8并行回收更快减少对象大小优化代码减少大对象减少GC压力使用对象池复用对象减少创建减少GC频率6.3 代码层面的优化// 不好的代码频繁创建大对象publicvoidbadCode(){byte[]buffernewbyte[10*1024*1024];// 使用buffer...}// 好的代码复用对象publicclassGoodCode{privatebyte[]buffernewbyte[10*1024*1024];publicvoidmethod(){// 复用buffer...// 注意需要处理线程安全问题}}// 更好的代码使用对象池publicclassBetterCode{privatestaticfinalObjectPoolbyte[]poolnewObjectPool(()-newbyte[10*1024*1024],10);publicvoidmethod(){byte[]bufferpool.borrowObject();try{// 使用buffer...}finally{pool.returnObject(buffer);}}}七、FullGC日志解读实战7.1 正常的FullGC日志[Full GC (System.gc()) [PSYoungGen: 51200K-0K(58880K)] [ParOldGen: 102400K-51200K(204800K)] 153600K-51200K(263680K), [Metaspace: 10240K-10240K(106496K)], 0.5234567 secs]解读System.gc()触发原因51200K-0K(58880K)新生代回收前51200K回收后0K102400K-51200K(204800K)老年代回收前102400K回收后51200K153600K-51200K(263680K)堆总回收前153600K回收后51200K0.5234567 secs耗时0.52秒7.2 异常FullGC日志[Full GC (Ergonomics) [PSYoungGen: 51200K-51200K(58880K)] [ParOldGen: 102400K-102400K(204800K)] 153600K-153600K(263680K), 0.8234567 secs]问题回收前后内存没有变化51200K-51200K说明内存泄漏或内存真的不够用。7.3 连续的FullGC[Full GC (Metadata GC Threshold) ...] # 第一次 [Full GC (Metadata GC Threshold) ...] # 第二次间隔1秒 [Full GC (Metadata GC Threshold) ...] # 第三次间隔1秒问题元空间不足每次FullGC回收一点但很快又满了。解决方案增大元空间-XX:MaxMetaspaceSize512m八、总结8.1 FullGC的核心要点要点说明定义对整个堆新生代老年代的完整垃圾回收触发条件老年代不足、元空间不足、System.gc()、担保失败等耗时几百毫秒到几秒会造成应用卡顿优化目标减少FullGC频率缩短FullGC耗时8.2 FullGC触发条件速记老年代满空间不足 元空间满类太多 System.gc显式调用 担保失败MinorGC前 CMS/G1并发失败8.3 优化思路速记增大堆内存让空间更大 调整分代比减少晋升频 禁用显式GC防止人为触 优化代码减少大对象 选对GC器G1/ZGC低停顿九、面试金句如果面试官问你FullGC是怎么触发的如何优化你可以这样回答“FullGC是对整个堆的完整回收触发条件主要有老年代空间不足、元空间不足、显式调用System.gc()、担保机制失败、以及CMS或G1的并发回收失败。优化FullGC的核心是减少触发频率和缩短回收时间具体措施包括合理设置堆大小-Xmx、调整分代比例-Xmn、禁用显式GC-XX:DisableExplicitGC、选择合适的垃圾回收器G1/ZGC、以及代码层面减少大对象创建。最重要的是要通过GC日志分析定位具体原因而不是盲目调整参数。”如果你觉得本文有帮助欢迎点赞、评论、转发你的支持是我持续输出的动力。