G1(Garbage-First)收集器是垃圾收集技术发展史上的一个里程碑。它旨在替代CMS,成为服务端、大内存、多处理器场景下的默认收集器。从JDK 9开始,G1被确立为默认的垃圾收集器。
核心目标与设计理念
G1的核心设计目标是在延迟可控的情况下,尽可能提高吞吐量。与之前的收集器不同,G1摒弃了传统的连续物理分代(新生代、老年代)设计,而是采用了一种基于Region的堆内存布局和可预测的停顿时间模型。
革命性变化:Region分区模型
G1将整个Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演Eden空间、Survivor空间或Old空间。Region的大小可以通过 -XX:G1HeapRegionSize 设置,取值范围为1M到32M,且应为2的N次幂。
这种设计使得G1不再坚持固定大小和固定数量的分代划分。它能够将收集范围限制在多个Region之间,而无需针对整个新生代或老年代。这使得G1可以建立可预测的停顿时间模型:让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒(通过参数 -XX:MaxGCPauseMillis 指定,默认200ms)。
G1会跟踪各个Region里面的垃圾堆积的“价值”大小(即回收所能获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的那些Region。这就是“Garbage-First”名字的由来。
G1的执行过程
G1的运作过程大致可分为以下几个阶段,其中某些阶段与CMS类似。
1. 年轻代GC (Young GC)
- 触发条件:当Eden区的Region被耗尽,无法为新对象分配空间时,G1会触发一次年轻代GC。
- 过程:
- 根扫描:扫描GC Roots(STW)。
- 更新RSet:处理 Dirty Card Queue 中的记录,更新 RSet(STW)。此阶段会完成对RSet的更新(详见下文)。
- 处理RSet:识别被老年代对象指向的Eden区对象,这些对象是存活对象(STW)。
- 对象拷贝:采用复制算法,将存活的对象从Eden区和From Survivor区拷贝到To Survivor区(一个新的Region),或者晋升(Promote) 到老年代的Region(STW)。
- 清空Region:回收整个Eden区和已使用的Survivor区。
- 特点:这是一个完全STW的并行收集过程,由多个GC线程同时完成。
2. 并发标记周期 (Concurrent Marking Cycle)
这是G1回收老年代Region的核心阶段,它是一个相对独立的过程,并非每次Young GC都会触发。其步骤与CMS非常相似但更为复杂:
a) 初始标记 (Initial Mark)
- 任务:仅仅标记一下GC Roots能直接关联到的对象,并修改TAMS指针,为下一阶段并发标记做准备。
- 状态:需要 STW。
- 技巧:这个阶段借用于一次正常的Young GC,因此没有额外的、明显的停顿成本。
b) 根区域扫描 (Root Region Scanning)
- 任务:扫描在初始标记中标记为存活的对象所在的Survivor Region(根区域),这些对象可能引用了老年代的对象。
- 状态:并发执行,但必须在下次Young GC开始前完成,否则Young GC必须等待。
c) 并发标记 (Concurrent Marking)
- 任务:从GC Roots开始,进行可达性分析,递归扫描整个堆里的对象图,找出所有存活的对象。
- 状态:并发执行。耗时最长,但与应用线程一起运行。
- 关键技术:采用SATB(Snapshot-At-The-Beginning) 算法来解决对象消失问题(详见下文)。
d) 最终标记 (Final Marking)
- 任务:处理在并发标记期间,由于用户程序运行而导致变动的少量SATB日志记录,并完成存活对象的标记。
- 状态:需要 STW。此阶段会进行全局引用处理(如类卸载)和空Region的回收。
e) 清理阶段 (Cleanup)
- 任务:
- 统计每个Region中完全空闲的Region和存活对象比例。
- 根据用户期望的停顿时间,对Region进行排序,确定一个回收计划。
- 更新记忆集(RSet),例如,处理那些指向即将被回收的Region的卡页。
- 状态:此阶段部分STW(统计和排序),部分并发(RSet清理)。
3. 混合回收 (Mixed GC)
- 触发条件:当老年代Region的堆占用比例达到阈值(-XX:InitiatingHeapOccupancyPercent,默认45%)时,在并发标记周期结束后,并不会立即开始Mixed GC,而是会开始一系列以Mixed GC为主的收集阶段。
- 过程:Mixed GC会同时回收一部分年轻代的Region和一部分老年代的Region(这些老年代Region是在并发标记周期中被识别为垃圾比例最高的)。回收的过程与Young GC类似,也是进行对象拷贝(复制算法)。
- 目标:尽可能地在用户指定的停顿时间内,回收尽可能多的垃圾区域。
4. Full GC
G1的设计目标是尽量避免Full GC。但如果对象分配速度过快,在Mixed GC来不及回收时,或者并发标记周期完成前老年代就被填满,就会导致并发失败(Evacuation Failure),从而触发一次完全STW的、使用单线程的标记-整理算法的Full GC。这是G1的失败预案,性能极差,需要尽力避免。
下图展示了G1收集器从You
ng GC到并发标记,再到Mixed GC的完整工作流程与状态转换:
核心技术:RSet、卡表与SATB
记忆集(Remembered Set, RSet)
在分代收集中,年轻代GC需要扫描老年代来确认跨代引用,这是非常低效的。G1为每个Region都维护了一个记忆集(RSet)。
- 作用:避免全堆扫描。RSet记录了其他Region中的对象指向本Region内对象的引用。
- 原理:这是一种“空间换时间”的策略。当进行年轻代GC时,只需要选定年轻代Region的RSet作为GC Roots的一部分,就可以找到所有来自老年代的引用,而无需扫描整个老年代。这极大地减少了GC的工作量。
卡表(Card Table)与RSet的实现
RSet在内部的实现通常是一种卡表(Card Table) 的扩展结构,可以理解为一种“反向的卡表”。
- 传统卡表:记录“老年代→新生代”的引用。
- G1的RSet:记录“其他Region→本Region”的引用,是一种“点对点”的更精细的结构。
- 写屏障(Write Barrier):为了实现RSet,G1在引用字段赋值操作(如 objA.field = objB)前后插入了额外的代码(写屏障)。这个屏障会判断引用是否跨Region(例如,objB在Region A,而objB在Region B)。如果是跨Region引用,就会将对应引用信息记录到被引用Region(Region B)的RSet中。
SATB:解决并发标记的“对象消失”问题
与CMS使用增量更新(Incremental Update) 不同,G1在并发标记阶段使用 SATB(Snapshot-At-The-Beginning) 算法来保证标记的正确性。
- 原理:SATB认为,在并发标记开始时(初始标记阶段),堆中所有存活的对象构成一个逻辑上的“快照”。在并发标记过程中,如果某个存活对象被删除了(即变为垃圾),G1会通过写屏障将这种变化记录下来。
- 具体操作:在引用赋值前,写屏障会将被覆盖的旧引用值(即将被删除的引用)记录下来,放入一个专门的缓冲区。在最终标记阶段(STW),G1会将这些记录下的旧引用作为根,重新扫描一次。确保在快照中存活的对象,即使在并发阶段被删除了引用,也不会被错误回收。
- ** vs CMS:CMS关注的是新增的引用(增量更新),而G1关注的是被删除**的引用(SATB)。SATB的效率通常更高,因为在并发阶段,产生的删除记录通常比新增记录要少。
G1的优缺点与调优
优点
- 可预测的停顿:能通过 -XX:MaxGCPauseMillis 设定目标停顿时间。
- 高吞吐量与低延迟:兼顾了高吞吐量和低停顿。
- 无内存碎片:从整体上看是基于“标记-整理”算法(Region间),从局部(两个Region之间)看是基于“复制”算法,这意味着G1运作期间不会产生内存空间碎片。
缺点与调优
- 内存占用与额外执行负载(Footprint & Overhead):
- 问题:RSet和卡表维护需要占用额外的内存(通常为堆大小的10%~20%)。写屏障也会给程序运行带来额外的负担。
- 调优:监控RSet的大小,通常无需手动干预。
- 并发失败(Evacuation Failure)与Full GC:
- 问题:这是G1最需要避免的问题。通常由对象晋升过快或巨型对象分配导致。
- 调优:
- 增加堆大小或更早地触发标记周期(降低 -XX:InitiatingHeapOccupancyPercent)。
- 增加并发标记的线程数(-XX:ConcGCThreads)。
- 避免巨型对象:G1对大对象(大小超过Region一半的对象)有特殊处理,会将其放入专门的Humongous Region。频繁分配/回收大对象会严重影响性能。
- ** Mixed GC回收效率不足**:
- 问题:垃圾很多,但每次Mixed GC回收的Region却不多。
- 调优:降低 -XX:G1MixedGCLiveThresholdPercent(默认85%),让存活对象比例超过85%的Region也可以被回收。增加 -XX:G1MixedGCCountTarget,让一次Mixed GC周期内能进行更多次的混合收集。
总结
G1是一款革命性的收集器,它通过Region分区模型、可预测的停顿时间和高效的并发标记,成功地在延迟和吞吐量之间找到了一个优秀的平衡点。其核心机制——RSet实现了精准的跨代引用跟踪,SATB算法解决了并发标记的准确性难题——不仅是G1的基石,也深刻影响了后续ZGC等新一代收集器的设计。虽然其调优参数比CMS更为复杂,但理解和掌握G1,是迈向JVM性能调优专家之路的关键一步。