【JVM原理详解】28-G1收集器深度解析
28-G1 收集器深度解析CMS 用并发回收把停顿降到了百毫秒级但留下了两个硬伤内存碎片和Full GC 不可控。当堆增长到 8GB、16GB 甚至更大CMS 的 Full GC退化为 Serial Old会变成几秒甚至十几秒的灾难。G1Garbage First收集器用分区化Region-based设计彻底解决了这两个问题它把堆切成数百个大小相等的 Region按回收价值优先回收垃圾最多的区域——这就是 “Garbage First” 名字的由来。G1 是JDK 9 的默认收集器本篇深入剖析其内存布局、CSet/RSet/SATB 机制、GC 流程与调优实战。G1 的内存布局Region 分区G1 打破了传统的连续新生代 连续老年代布局把整个堆划分为大小相等的 Region默认 1-32MB2 的幂。每个 Region 在运行时动态扮演不同角色堆16GB假设 Region8MB共约 2048 个 Region ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │E │E │S │O │O │H │H │O │E │ │O │S │ ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ │O │E │O │O │H │ │E │O │O │S │O │O │ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ EEden SSurvivor OOld HHumongous 空Free四种 Region 类型Eden新生代刚分配的对象。SurvivorGC 后存活的新生代对象。Old老年代多次 GC 后仍存活的对象。Humongous大对象专用超过 Region 一半大小的对象直接分配在这里。为什么分区化是革命性的传统收集器的代际是连续内存块回收要么全收、要么不收。G1 的 Region 是逻辑上的代——同一时刻 Eden Region 可以散布在堆的任意位置。这让 G1 能选择性回收只回收垃圾最多的几个 Region而不必每次扫全堆。传统 CMSFull GC 时扫整个老年代 → 堆越大越慢 G1 Mixed GC只回收垃圾最多的 10 个 Old Region → 停顿可控这就是 G1 能在大堆上保持可控停顿的根本原因回收范围与堆大小解耦。Humongous Region大于 Region 一半的对象如 5MB 数组在 8MB Region 下直接进 Humongous Region可能跨越多个连续 Region。大对象单独管理的好处是不占用 Eden/Survivor 空间避免频繁晋升触发 GC。回收时整体处理简单高效。// Region 8MB 时下面这些都会进 Humongousbyte[]big1newbyte[5*1024*1024];// 5MB 4MB(一半)byte[]big2newbyte[16*1024*1024];// 16MB占 2 个连续 RegionCSet 与 RSetG1 的核心数据结构是CSetCollection Set和RSetRemembered Set。CSet本次回收的 Region 集合CSet 是本次 GC 要回收的 Region 列表。G1 根据停顿目标和回收价值Region 里垃圾占比动态选择停顿目标 200msRegion 回收耗时约 10ms/个 → CSet 约 20 个 Region 选择策略按垃圾占比降序选Garbage First Region A: 90% 垃圾 → 优先选 Region B: 80% 垃圾 → 选 Region C: 30% 垃圾 → 不选性价比低Young GC 时 CSet 所有 Eden Survivor RegionMixed GC 时 CSet 所有 Young 部分 Old Region选垃圾最多的 Old。RSet记录谁引用了我传统分代回收通过Card Table记录跨代引用。G1 的 Region 是动态的跨 Region 引用更复杂需要更精细的结构——RSet。RSet 的思路是每个 Region 维护一个指向我的引用来源列表。Region X 被引用情况RSet Region A.obj5 → Region X.obj1 Region C.obj2 → Region X.obj3 ...回收 Region X 时只要扫它的 RSet 就知道哪些外部对象指向我不必扫全堆找 roots——这是 G1 能局部回收的关键。没有 RSet回收 Region X 要扫描整个堆找谁引用 X → 全堆扫描停顿爆炸 有 RSet回收 Region X 只扫 X 的 RSet → 停顿可控RSet 的代价是内存开销通常占堆 1-20%和写屏障开销——每次引用写入都要更新 RSet。G1 用写屏障Write Barrier拦截写操作// 伪代码G1 写屏障voidfieldWrite(Objectsrc,Fieldf,Objectdst){RegionsrcRregionOf(src);RegiondstRregionOf(dst);if(srcR!dstR){dstR.rememberedSet.add(srcR,offset(src,f));// 记录跨 Region 引用}// 实际写入src.fdst;}SATB并发标记的快照G1 的并发标记和 CMS 一样面临引用变化问题但解决方案不同——SATBSnapshot-At-The-Beginning。思路并发标记开始时拍一张存活对象快照标记期间即使引用断开原快照里的对象仍按存活处理本轮不回收。并发标记开始时刻 T 对象图状态 → 快照 S 标记期间应用在跑 断开引用A.field null原本 A→B SATB 行为B 仍按在 T 时刻被引用处理 → 本轮不回收 B → B 成为浮动垃圾下次再回收SATB 与增量更新的区别机制解决的问题处理时机数据结构增量更新CMS黑色对象新引用白色对象重新标记时重扫Card TableSATBG1灰色对象断开到白色对象的引用并发期间实时记录SATB 队列SATB 在写屏障中记录被覆盖的旧引用// 伪代码SATB 写屏障voidfieldWrite(Objectsrc,Fieldf,ObjectnewValue){ObjectoldValuesrc.f;// 先读旧值if(oldValue!null){satbQueue.add(oldValue);// 加入 SATB 队列标记结束统一处理}src.fnewValue;}快照保证旧引用的对象在快照时刻是存活的本轮按存活处理绝不漏标。代价是浮动垃圾本可回收但要等下轮但正确性优先。GC 流程G1 有三种 GC 模式按触发条件切换。Young GC当 Eden 区填满时触发。CSet 所有 Eden Survivor Region复制存活对象到新的 Survivor Region。Young GC 前 [Eden1][Eden2][Eden3][S0][O1][O2][O3] Young GC 后存活对象进 Survivor [ ][ ][ ][ ][O1][O2][O3][S1][S1] ← Eden 清空 → ← 原 Eden 存活对象 →Young GC 是 STW 的但因为只回收 Young Region数量受控停顿可通过MaxGCPauseMillis控制。这是 G1 的常规操作频率最高。Mixed GC当老年代占用率达到阈值-XX:InitiatingHeapOccupancyPercent默认 45%时G1 触发并发标记标记完成后进入 Mixed GC 阶段。Mixed GC 的 CSet 所有 Young 部分 Old Region选垃圾最多的。Mixed GC 流程 1. 并发标记不停应用── 遍历 Old Region 找垃圾 2. 重新标记STW────── 补标 SATB 队列 3. 独占清理STW────── 统计垃圾占比选 CSet 4. 多次 Mixed GC ─────── 每次回收部分 Old RegionMixed GC 是 G1 回收老年代的核心方式——分批回收 Old Region避免一次性全老年代 Full GC。通常一次 Mixed GC 回收 10-30% 的 Old Region多次才能清完。Full GC退化当 Mixed GC 跟不上分配速度老年代耗尽时G1 退化为Single-threaded Full GCSerial Old 算法STW 整理全堆。这是 G1 的失败兜底应该极力避免。正常Young GC → Mixed GC → Young GC → Mixed GC ...停顿可控 异常分配太快 → Mixed GC 来不及 → Full GC灾难秒级停顿JDK 10 后 G1 的 Full GC 改为多线程并行缓解了停顿但仍不理想。正确调优应保证 Full GC 不发生。-XX:MaxGCPauseMillis 调优G1 最核心的调优参数是停顿目标java-XX:MaxGCPauseMillis200-XX:UseG1GC-cpMyApp com.example.Main默认 200ms。G1 根据这个目标动态调整 CSet 大小停顿目标小就少选几个 Region目标大就多选几个。MaxGCPauseMillis50 → CSet 小 → 回收快但不彻底 → GC 频繁 MaxGCPauseMillis500 → CSet 大 → 回收彻底 → 单次停顿长调优的平衡点# 生产推荐4GB 堆java-Xms4g-Xmx4g-XX:UseG1GC\-XX:MaxGCPauseMillis200\-XX:InitiatingHeapOccupancyPercent45\-XX:G1HeapRegionSize8m\-XX:G1NewSizePercent20\-XX:G1MaxNewSizePercent60\-XX:G1MixedGCCountTarget8\-Xlog:gc*info:filegc.log-cpMyApp com.example.Main参数说明MaxGCPauseMillis200200ms 是多数 Web 服务可接受的上限。InitiatingHeapOccupancyPercent45堆占用 45% 就开始并发标记默认值合理。G1HeapRegionSize8mRegion 大小G1 会根据堆自动选 1/2/4/8/16/32MB。G1NewSizePercent20新生代最小占比防止被压得太小。G1MaxNewSizePercent60新生代最大占比留空间给 Mixed GC。G1MixedGCCountTarget8Mixed GC 分 8 次完成每次回收少点 Old Region停顿更可控。停顿目标不是越低越好# 错误盲目调低-XX:MaxGCPauseMillis1010ms 目标下 G1 会把 CSet 压到极小导致回收不彻底、GC 频繁飙升反而降低吞吐量。根据 SLA 设定合理值Web 服务通常 100-300ms。代码示例观察 G1 行为/** * 演示 G1 Young GC 与 Mixed GC * 适用 JDK 11/17 * * 运行 * java -Xms2g -Xmx2g -XX:UseG1GC * -XX:MaxGCPauseMillis200 * -XX:G1HeapRegionSize4m * -Xlog:gc*info,gcheapdebug -cp MyApp G1GcDemo */publicclassG1GcDemo{staticfinalint_1MB1024*1024;staticfinaljava.util.Listbyte[]CACHEnewjava.util.ArrayList();publicstaticvoidmain(String[]args)throwsException{// Phase 1快速分配触发 Young GCfor(inti0;i100;i){byte[]blocknewbyte[256*1024];Thread.sleep(5);}// Phase 2保留引用填充老年代触发 Mixed GCfor(inti0;i200;i){CACHE.add(newbyte[512*1024]);if(i%200)CACHE.subList(0,CACHE.size()/3).clear();Thread.sleep(2);}}}日志关键字段# Young GC [0.234s][info][gc,start] GC(0) Pause Young (Normal) (G1 Evacuation Pause) [0.234s][info][gc,heap] GC(0) Eden regions: 40-0(40) [0.235s][info][gc] GC(0) Pause Young (Normal) 500M-200M(2048M) 5.678ms # 并发标记开始Mixed GC 前奏 [1.234s][info][gc] GC(5) Concurrent Cycle [1.234s][info][gc,marking] GC(5) Concurrent Clear Claimed Marks [1.235s][info][gc,marking] GC(5) Concurrent Scan Root Regions [1.300s][info][gc,marking] GC(5) Concurrent Mark (1.234s, 1.300s) # Mixed GC [1.567s][info][gc,start] GC(8) Pause Young (Mixed) (G1 Evacuation Pause) [1.567s][info][gc,heap] GC(8) Old regions: 30-25 [1.568s][info][gc] GC(8) Pause Young (Mixed) 1200M-800M(2048M) 12.345ms注意日志中(Normal)表示 Young GC(Mixed)表示 Mixed GC。Old regions: 30-25说明这次 Mixed GC 回收了 5 个 Old Region。G1 vs CMS 对比维度CMSG1内存布局连续分代Region 分区老年代算法Mark-SweepMark-Compact复制碎片问题严重无Region 复制整理Full GCSerial Old 降级多线程并行JDK 10停顿可控性较差Concurrent Mode Failure好CSet 动态调整标记算法增量更新SATB适用堆大小 4GB4-32GBJDK 支持JDK 814 移除JDK 9 默认调优复杂度高参数多中自适应为主G1 的核心优势是用 Region 复制代替全堆整理让大堆也能保持可控停顿。CMS 在 4GB 以下堆仍有竞争力但 G1 是面向未来的选择。JDK 9 默认收集器从 JDK 9 开始不显式指定-XX:UseG1GC时默认就是 G1。这意味着大多数应用开箱即用无需调参。MaxGCPauseMillis200的默认值对多数场景合理。只有特殊场景超低延迟、超大堆、批处理才需要换收集器。JDK 11 进一步优化了 G1并发标记的停顿从多次 STW 优化为单次减少了 STW 次数。JDK 12 引入了可中断 Mixed GCJEP 344超时能中止避免停顿超标。JDK 17 的 G1 在 NUMA 感知上也有改进。实践要点1. 不要盲目调小停顿目标# 错误10ms 目标导致 GC 风暴-XX:MaxGCPauseMillis10G1 会为达成目标疯狂缩小 CSet导致回收不彻底、频率飙升。根据 SLA 设 100-300ms。2. IHOP 阈值调整默认InitiatingHeapOccupancyPercent45即堆用到 45% 就开始并发标记。如果 Mixed GC 来不及可调低到 35-40提早启动回收。3. Region 大小的选择G1 自动选择 Region 大小但某些场景需手动干预# 大对象多时调大 Region 减少 Humongous 开销-XX:G1HeapRegionSize16m# 堆小 2GB时保持默认1-2MB即可4. 监控 Full GCG1 出现 Full GC 说明调优失败。日志里Pause Full (G1 Compaction Pause)就是红线。常见原因老年代太小、Mixed GC 速度跟不上分配、大对象洪流。5. Mixed GC 的目标次数G1MixedGCCountTarget默认 8即 Mixed GC 分 8 次清完老年代垃圾。调大如 16让每次回收更少、停顿更短调小如 4清得更快但单次停顿长。6. 不要关闭自适应# 错误手动固定新生代-XX:G1NewSizePercent50-XX:G1MaxNewSizePercent50除非有充分理由不要强制固定新生代比例——G1 的自适应能根据负载动态优化手动干预往往帮倒忙。小结G1用Region 分区打破连续分代布局把堆切成数百个 1-32MB 的 Region动态扮演 Eden/Survivor/Old/Humongous 角色。CSet是本次回收的 Region 集合RSet记录谁引用了我让局部回收成为可能两者共同支撑了 G1 的按价值优先回收策略。SATB在并发标记开始时拍快照通过写屏障记录旧引用保证并发标记正确性代价是浮动垃圾。GC 流程Young GC常规→Mixed GC回收部分 Old→Full GC失败兜底应极力避免。核心调优MaxGCPauseMillis设 100-300msIHOP45大对象多时调大G1HeapRegionSize保持自适应开启。JDK 9 默认 G1对 4-32GB 堆的低延迟场景是当前最佳选择CMS 在 JDK 14 后已移除。下一篇我们将探索更前沿的低延迟收集器——ZGC 与 Shenandoah它们把停顿目标从百毫秒推向亚毫秒是 GC 技术的当代前沿。更多内容JVM调优实战