Garbage-First (G1) 垃圾回收器是 Java HotSpot 虚拟机中一种面向服务端应用的、旨在实现低暂停时间目标的垃圾回收器。与传统的 CMS 或 Parallel Scavenge 不同,G1 的一个核心创新在于它将 Java 堆划分为一系列大小相等的独立区域(Region)。每个 Region 可以扮演 Eden、Survivor 或 Old Generation 的角色。
这种基于 Region 的设计带来了巨大的灵活性,使得 G1 可以增量地进行垃圾回收,不必每次都回收整个年轻代或老年代。G1 的目标是在用户设定的暂停时间目标内(通过 -XX:MaxGCPauseMillis
),优先回收那些**垃圾最多(Garbage First)**的 Region,从而获得最高的回收效率。
然而,这种按需回收部分 Region 的能力也带来了新的挑战:
为了解决这两个核心问题,G1 引入了两个关键机制:
阅读前提: 假设已经了解 JVM 内存结构(堆、栈等)、基本的垃圾回收概念(可达性分析、分代假设),并对 G1 GC 的 Region 划分有初步认识。
想象一下,如果没有 RSet,当 G1 决定回收某个 Old Region A 时,它如何知道这个 Region A 里的对象是否被其他 Old Region B 里的对象引用着?最笨的方法是扫描所有其他的 Old Region,查找指向 A 的引用。但这显然违背了 G1 避免全堆扫描的设计初衷,会导致漫长的 STW。
RSet 的核心目的: 避免全堆扫描,能够快速、准确地识别出哪些其他 Region 中的对象引用了当前 Region 中的对象。
RSet 的概念: 每个 G1 Region 都有一个与之关联的 RSet。这个 RSet 记录了其他 Region 指向该 Region 的引用信息。更具体地说,RSet 存储的是指向本 Region 的对象的引用所在的 Card 的索引。
理解难点与类比:
所以,当 G1 要回收 Region A 时,它只需要扫描 Region A 自己的 RSet,就能知道所有从外部指向 Region A 的引用入口(精确到 Card 级别)。然后从这些入口(Card 对应的内存区域)出发,结合 GC Roots,就能判断 Region A 内部哪些对象是真正存活的。
RSet 既然记录了跨 Region 的引用信息,那么当应用程序修改对象引用时,RSet 就必须被及时、准确地更新。否则,如果一个跨 Region 引用被删除,而 RSet 没有更新,可能会导致不必要的对象存活;如果一个新的跨 Region 引用被建立,而 RSet 没有记录,则可能导致存活对象被错误回收(这是绝对不允许的)。
G1 通过以下机制来维护 RSet:
(a) 写屏障 (Write Barrier) - 信息的源头
a.field = b;
)时,JVM 会插入一段额外的代码,称为写屏障。a
和对象 b
是否位于不同的 Region。a
和 b
在同一个 Region,或者根据某些优化规则(后面会讲),这个引用不需要记录,则写屏障直接结束。a
所在的 Card 标识)放入一个线程本地的缓冲区,称为更新日志缓冲区 (Update Log Buffer) 或 脏卡片队列 (Dirty Card Queue)。(b) 更新日志缓冲区 / 脏卡片队列 - 信息的暂存
© 并发优化线程 (Concurrent Refinement Threads) - 信息的处理者
-XX:G1ConcRefinementThreads
参数设置并发优化线程的数量(默认通常等于并行 GC 线程数 -XX:ParallelGCThreads
)。(d) 处理反压 (Back-Pressure) - 应对极端情况
-XX:G1ConcRefinementGreenZone
-XX:G1ConcRefinementYellowZone
-XX:G1ConcRefinementRedZone
理解难点:为何需要如此复杂的异步更新机制?
直接在写屏障里更新 RSet 看似简单,但有几个缺点:
通过异步处理,G1 将 RSet 更新的重担交给了后台的并发优化线程:
为了进一步减少 RSet 的大小和维护开销,G1 实施了一些优化策略,并非所有跨 Region 引用都需要记录在 RSet 中:
RSet 的具体实现比较复杂,可能有多种形式,例如:
选择哪种结构取决于 RSet 的大小和密度,G1 会根据实际情况动态调整。
RSet 解决了“如何找到指向特定 Region 的引用”的问题,使得 G1 可以独立评估每个 Region 的存活性。但 G1 并不会在一次 GC 中回收所有可回收的 Region。为了满足用户设定的暂停时间目标,G1 必须精心选择一部分 Region 来构成单次 STW 回收的集合。这个集合就是收集集合 (Collection Set, CSet)。
CSet 的核心定义: 在一次 GC 暂停 (STW) 期间,G1 将要回收(Evacuate)的所有 Region 的集合。
CSet 的工作机制:
CSet 的具体内容取决于触发的是哪种类型的 GC:
(a) 年轻代收集 (Young Collection) 的 CSet
(b) 混合收集 (Mixed Collection) 的 CSet
-XX:InitiatingHeapOccupancyPercent
,默认 45%) 时,G1 会启动一个混合收集周期 (Mixed GC Cycle)。这个周期包含并发标记阶段和多次 Mixed GC 暂停。混合收集中,选择哪些 Old Region 加入 CSet 是 G1 实现“Garbage First”和控制暂停时间的关键。这个选择过程基于并发标记的结果和一系列启发式规则:
-XX:G1MixedGCLiveThresholdPercent
, 默认 85%): 如果一个 Old Region 的存活对象比例过高(例如超过 85%),回收它需要复制大量存活对象,耗时长且收益低。G1 会跳过这些 Region,不将它们加入 CSet。-XX:MaxGCPauseMillis
) 和之前 GC 的统计数据,预测本次回收能处理多少个 Old Region 而不超时。它会从排序好的高收益 Region 中选取一部分,直到预测的暂停时间接近目标值。-XX:G1OldCSetRegionThresholdPercent
, 默认 10%): 为了避免单次 Mixed GC 回收过多的 Old Region 导致暂停时间过长或 RSet 更新压力过大,G1 限制了单次 Mixed GC 中可以包含的 Old Region 数量,不能超过堆总大小的一定百分比。通过这些步骤,G1 精心挑选出一组 Old Region 加入到本次 Mixed GC 的 CSet 中,力求在满足暂停时间目标的前提下,最大化垃圾回收量。
-XX:G1MixedGCCountTarget
(默认 8): 一个 Mixed GC Cycle 中,预期执行多少次 Mixed GC Pause 来处理 Old Region。G1 会将候选的 Old Region 分摊到这么多次 Pause 中。-XX:G1HeapWastePercent
(默认 5%): 当 G1 发现经过一轮 Mixed GC 后,堆上可回收的垃圾比例低于这个值时,即使还没达到 G1MixedGCCountTarget
的次数,也可能提前结束 Mixed GC Cycle,认为再进行 Mixed GC 的收益不大了。现在我们将 RSet 和 CSet 放在 G1 的实际 GC 周期中,看看它们如何协同工作。
-XX:TargetSurvivorRatio
) 和对象的年龄分布,动态计算本次 GC 的晋升年龄阈值 (-XX:MaxTenuringThreshold
是上限)。这是一个更复杂的过程,RSet 和 CSet 在其中扮演关键角色:
IHOP
阈值。G1HeapWastePercent
限制,则开始执行一次或多次 Mixed GC Pause。G1MixedGCCountTarget
次数限制和 G1HeapWastePercent
比例限制,则继续执行下一次 Mixed GC Pause (步骤 6)。理解关键点:
直接展示 HotSpot 中 G1 的 RSet 和 CSet 源码对于我们可能过于复杂和深入。这里提供一些概念性的 C++ 伪代码片段,帮助理解核心逻辑,并附上中文注释。(AI生成,我看不懂这块的源代码,只能借助AI生成几个场景了,不保证绝对正确,但是我看下来没什么问题= = )
注意: 这些是高度简化的示意代码,与真实源码有很大差异。
// 伪代码:对象引用赋值 a.field = b 之后触发的 G1 后写屏障
void G1PostWriteBarrier(oop a, oop b) {
// 1. 获取对象 a 所在的 Card 的标识 (card_ptr)
CardValue* card_ptr = get_card_ptr(address_of(a));
// 2. 检查是否已经是脏卡片 (避免重复处理)
if (*card_ptr == DIRTY_CARD_VALUE) {
return; // 已经是脏的,无需处理
}
// 3. [优化检查 - 实际更复杂] 检查是否需要记录
// a. 是否跨 Region? (is_cross_region(a, b))
// b. 是否是从 Old 指向 Young (通常需要记录)
// c. 是否是从 Young 指向 Young (通常不需要记录)
// d. 是否是从 Old 指向 Old (通常需要记录)
// if (!needs_rset_update(a, b)) {
// return; // 根据规则,此引用无需记录到 RSet
// }
// 4. 标记 Card 为脏
*card_ptr = DIRTY_CARD_VALUE;
// 5. 将脏卡片信息放入当前线程的 Update Log Buffer
Thread* current_thread = Thread::current();
if (!current_thread->dirty_card_queue().enqueue(card_ptr)) {
// 如果本地队列满了,将其提交到全局队列
submit_dirty_card_queue_to_global_list(current_thread);
// 再次尝试入队 (通常会成功,因为队列已清空)
current_thread->dirty_card_queue().enqueue(card_ptr);
}
}
// 伪代码:并发优化线程 (Concurrent Refinement Thread) 的主循环
void ConcurrentRefinementThread::run() {
while (should_continue_running()) {
// 1. 从全局列表中获取一个待处理的脏卡片队列 (Buffer)
DirtyCardQueue* buffer = G1GlobalDirtyCardQueueList::dequeue();
if (buffer != NULL) {
// 2. 处理该 Buffer 中的每一个脏卡片
CardValue* card_ptr;
while ((card_ptr = buffer->dequeue()) != NULL) {
// a. 根据 Card 地址找到其所在的源 Region (Source Region)
HeapRegion* source_region = heap->heap_region_containing(card_ptr);
// b. 遍历这个 Card 覆盖的内存范围内的所有对象
iterate_objects_in_card(card_ptr, [&](oop obj) {
// c. 遍历该对象的引用字段
iterate_reference_fields(obj, [&](oop target_obj) {
if (target_obj != NULL) {
// d. 获取目标对象所在的 Region (Target Region)
HeapRegion* target_region = heap->heap_region_containing(target_obj);
// e. 如果是跨 Region 引用,并且目标 Region 不是源 Region
if (target_region != source_region) {
// f. **更新目标 Region 的 RSet**
// 将源 Card (card_ptr) 的信息添加到 target_region 的 RSet 中
target_region->remembered_set()->add(card_ptr);
}
}
});
});
}
// 处理完 Buffer 后,回收 Buffer
recycle_buffer(buffer);
} else {
// 全局列表为空,线程可以短暂休眠或等待
wait_for_work();
}
}
}
// 伪代码:选择 Old Region 加入 Mixed CSet 的过程
void select_old_regions_for_mixed_cset(CollectionSet* cset, G1Policy* policy) {
// 1. 获取并发标记后确定的候选 Old Region 列表 (已按回收收益排序)
GrowableArray<HeapRegion*>& candidates = policy->candidate_old_regions();
// 2. 初始化预测的暂停时间和已添加的 Old Region 数量
double predicted_pause_time = policy->predict_young_gc_pause_time();
int added_old_region_count = 0;
size_t added_old_region_bytes = 0;
// 3. 计算 CSet 中 Old Region 的数量和大小上限
int max_old_regions_in_cset = calculate_max_old_regions(policy); // 基于 G1OldCSetRegionThresholdPercent
size_t max_old_bytes_in_cset = calculate_max_old_bytes(policy);
// 4. 遍历高收益的候选 Old Region
for (int i = 0; i < candidates.length(); ++i) {
HeapRegion* candidate = candidates.at(i);
// a. 检查活跃度阈值
if (candidate->live_ratio() > policy->g1_mixed_gc_live_threshold_percent() / 100.0) {
continue; // 存活对象太多,跳过
}
// b. 预测加入这个 Region 会增加多少暂停时间
double predicted_increment = policy->predict_region_evacuation_time(candidate);
// c. 检查是否会超出暂停时间目标和 CSet 上限
if (predicted_pause_time + predicted_increment <= policy->max_gc_pause_millis() &&
added_old_region_count < max_old_regions_in_cset &&
added_old_region_bytes + candidate->used() < max_old_bytes_in_cset)
{
// d. 将该 Region 加入 CSet
cset->add(candidate);
added_old_region_count++;
added_old_region_bytes += candidate->used();
predicted_pause_time += predicted_increment;
} else {
// 如果加入这个 Region 会超时或超限,停止选择
break;
}
}
// CSet 确定完毕
}
这些简化代码旨在展示 RSet 维护(写屏障->缓冲->并发处理)和 CSet 选择(基于收益、阈值和暂停目标)的核心思想。
已记忆集合 (RSet) 和收集集合 (CSet) 是 G1 GC 实现其低暂停时间、高吞吐量目标的两大支柱。
Happy coding!