G1 GC 的核心基础:Region 模型的补充细节

一、G1 GC 的核心基础:Region 模型的补充细节

原文对Region的讲解很清晰,这里补充两个支撑G1高效运作的关键数据结构:

1.1 Remembered Set (RSet) ------ 分区独立的引用"账本"

G1之所以能独立回收某个Region,而不需要扫描全堆,核心就是RSet

  • 作用:每个Region都有一个RSet,记录着"有哪些Region里的对象,引用了本Region中的对象"。

  • 好处 :在回收Region时,GC线程只需扫描该Region的RSet,就能知道哪些外部对象在引用它,从而找到GC Roots。这彻底打破了传统收集器扫描整个代(如整个老年代)的瓶颈。

  • 代价:维护RSet需要额外内存(约占堆内存的5%~10%),且当引用关系变化时(比如对象赋值),需要同步更新RSet。这是G1对写屏障(Write Barrier)开销的主要来源。

1.2 Humongous 大对象区的特殊处理

原文提到了大对象区(H区),这里补充它的回收规则:

  • 判定 :当对象大小 > 50% G1HeapRegionSize时,会被分配为"巨型对象"。如果对象大小超过一个Region,会占用连续的多个H区。

  • 回收 :巨型对象不会 在Young GC中被回收。只有在Mixed GC的并发标记阶段 发现它不再存活,或者在Full GC时才会被回收。

  • 风险 :如果大对象频繁创建且生命周期短,很容易填满老年代,诱发Full GC。调优时建议通过jmap -histo查看大对象,必要时调整G1HeapRegionSize(让大对象落入普通老年代Region,享受Mixed GC回收),或从代码层面优化。


二、回收类型进阶:Mixed GC 的内部阶段细化

原文将Mixed GC分为并发标记和筛选回收两步。实际上,在JDK 8u40之后,G1的Mixed GC在"筛选回收"阶段内部也做了精细化拆分,这对理解停顿非常关键。

2.1 Mixed GC 的完整阶段(JDK 8u40+)

除了原文提到的"并发标记"五子阶段,后续的"筛选回收"阶段在G1中被称为清理阶段,它实际上包含两个子阶段:

  1. 并发标记阶段(同原文,略)

  2. 清理阶段

    • STW 清理 :计算每个老年代Region的垃圾占比,并排序。这个阶段只做统计,不回收对象。G1会预测如果回收某个Region需要多长时间。

    • 并发清理 :清空那些没有存活对象的Region(空Region),将它们放回空闲列表。此阶段无STW。

    • STW 筛选回收 :这才是真正的"复制存活对象"阶段。G1根据MaxGCPauseMillis目标,从排序后的Region列表中选择"性价比"最高的若干个(垃圾占比高且预测停顿短),进行复制和回收。注意,G1通常不会在一次Mixed GC中回收所有待回收的老年代Region,而是分多次(多轮)进行,直到老年代内存占比低于阈值。

2.2 为什么会有"多次"Mixed GC?

这是G1实现"软实时"目标的关键:

  • 一次并发标记结束后,会产生一个"待回收Region列表"。

  • 后续的GC暂停(可能连续多次)都是[GC pause (G1 Mixed Generation)]

  • 每次暂停只回收列表中的一小部分,直到列表为空,或者老年代内存占比降至目标值以下。


三、关键机制深挖:停顿预测模型与写屏障

3.1 停顿预测模型

G1的强大之处在于它有一个停顿预测模型。它不是简单地"回收几个Region",而是基于历史数据动态调整。

  • 原理 :JVM会记录每次回收时各阶段的耗时(如扫描RSet耗时、复制对象耗时)。在下次回收时,它会根据MaxGCPauseMillis目标,计算"当前我最多能回收多少个Region,才能保证99%的概率不超时"。

  • 调优启示 :如果MaxGCPauseMillis设置得过短(如<50ms),G1会极力压缩回收量,导致每次回收的垃圾太少,从而频繁触发Young GC和Mixed GC,整体吞吐量下降,甚至可能因回收速度跟不上分配速度而诱发Full GC。

3.2 SATB (Snapshot-At-The-Beginning) 标记算法

原文提到的并发标记阶段用到了SATB算法,它是G1能实现"并发标记"的底层支撑。

  • 作用:在并发标记开始时,SATB会为当前存活对象拍一张"逻辑快照"。

  • 逻辑 :在并发标记期间,如果某个原本存活的对象(在快照中)的引用被用户线程修改(比如被置为null),G1会通过写屏障 记录下这个对象,确保在最终标记阶段仍然将其视为存活

  • 结果 :这种"保守"的标记策略会导致浮动垃圾。即并发标记期间变成垃圾的对象,本次GC不会回收,只能等到下一次Mixed GC再处理。这是G1内存占用有时看起来"回收不彻底"的原因。


四、实战调优:从"拍脑袋"到"有依据"

原文给出了参数列表,这里补充这些参数的配合逻辑和反直觉场景。

4.1 核心参数组合拳:IHOPMaxGCPauseMillis 的博弈
  • -XX:InitiatingHeapOccupancyPercent (IHOP)

    • 默认值45% :指老年代占整个堆的比例。当老年代占用超过45%时,触发并发标记。

    • 调优场景 :如果GC日志中出现**"Concurrent Cycle"因"to-space overflow"中断(即并发标记中途老年代被填满)** ,说明IHOP设置得太高,Mixed GC启动太晚。应适当降低IHOP(如38%),给回收留出缓冲时间。

    • 注意 :在JDK 10之后,G1引入了自适应IHOP-XX:-G1UseAdaptiveIHOP),JVM会自动根据对象分配速率调整触发阈值,通常比手动设置更优。

4.2 线程数:ParallelGCThreadsConcGCThreads

这是调优中最容易忽略的参数。

  • 公式

    • ParallelGCThreads(STW阶段):如果CPU核数>8,公式为 8 + (核数-8)*5/8。通常不需要改。

    • ConcGCThreads(并发阶段):通常为 ParallelGCThreads 的 1/4。

  • 问题场景 :在CPU核数少(如2核)的容器环境中,如果ConcGCThreads默认为1,并发标记可能会占用大量CPU资源,导致业务线程争抢。此时可以考虑手动降低ConcGCThreads为1(已是下限),或通过容器CPU配额限制。

4.3 避免Full GC的"三板斧"

当发生Full GC时,不要只盯着内存大小,按顺序排查:

  1. 看日志 :是to-space overflow(并发回收速度跟不上分配)?还是concurrent mode failure(并发标记阶段老年代就满了)?

    • 前者:说明MaxGCPauseMillis太严苛,导致回收量小,或IHOP太高启动晚。

    • 后者:说明对象分配速率太高,考虑增大堆内存,或优化对象分配。

  2. 看大对象-XX:+PrintGCDetails 是否频繁出现 Humongous Allocation?如果是,考虑调整G1HeapRegionSize,将大对象"打散"进普通老年代。

  3. 看元空间 :G1的Full GC也可能由元空间(Metaspace)满触发。确保设置了 -XX:MaxMetaspaceSize,避免元空间无限制增长。


五、总结补充:G1的适用边界

原文总结得很好。最后补充一点关于"选型"的思考:

  • G1的优点 :它的设计目标是**"软实时"** ,即在可控的停顿时间(默认200ms) 内,完成尽可能多的垃圾回收。适合堆内存大(4GB+)、要求低延迟的应用。

  • G1的弱点

    • 维护成本高:RSet、卡表等元数据占用较多堆外内存。

    • 吞吐量稍低 :相比于Parallel Scavenge(吞吐量优先),G1由于写屏障和并发标记的开销,整体吞吐量(业务时间/总时间)会低约5%~10%

    • 小堆表现不佳:在堆内存较小(<4GB)且对吞吐量敏感的场景,Parallel Scavenge有时表现更好。

相关推荐
salipopl3 小时前
Spring 中的 @ExceptionHandler 注解详解与应用
java·后端·spring
LJianK13 小时前
java封装
java·前端·数据库
海海不瞌睡(捏捏王子)3 小时前
C#知识点概要
java·开发语言·1024程序员节
jessecyj3 小时前
SpringBoot详解
java·spring boot·后端
Flittly3 小时前
【SpringAIAlibaba新手村系列】(2)Ollama 本地大模型调用
java·ai·springboot
小王不爱笑1323 小时前
三色标记算法
算法
_MyFavorite_3 小时前
JAVA重点基础、进阶知识及易错点总结(10)Map 接口(HashMap、LinkedHashMap、TreeMap)
java·开发语言
qqty12173 小时前
Spring Boot管理用户数据
java·spring boot·后端
Flittly3 小时前
【SpringAIAlibaba新手村系列】(1)初识 Spring AI Alibaba 框架
java·spring