GC学习

GC学习

基础知识详见:垃圾回收

但是对G1的了解还不是特别深刻。这边记录一下面试过程中遇到的一些问题:

1

问:我们给堆分成了不同的区块,那么不同区块之间,是怎么区分这些区块的?

堆在启动时被切成等大小的 Region(1–32 MB,2 的幂;-XX:G1HeapRegionSize)。为了快速定位,

  • JVM保存了 regionLog = log2(regionSize)
  • 地址 → 区块编号regionId = (addr - heapBase) >>> regionLog(位移就能算,O(1))。

每个区块有一个 HeapRegion 元数据,记录:

  • 状态Free / Eden / Survivor / Old / HumongousStart / HumongousCont / Pinned …
  • bottom/end/top(指针推进的顶端)、活字节数RSet 指针、年龄/标记位等。
  • 也就是说,一个块他只可能属于一种状态,而不是多种状态的混合。

“年轻代/老年代”在 G1 里不是连续空间,而是若干 Region 的集合。因此任意地址都能通过上面的计算查到它属于哪个 Region、该 Region 是什么角色。

2

分配内存的时候,应该分配到哪一个区块?这是怎么区分的?

2.1 程序正常运行(Mutator)分配

  • 普通小对象(小于 Humongous 阈值,默认 < 1/2 Region):
    1. 先在 TLAB(线程本地分配缓冲)里走 bump-pointer
    2. TLAB 不够 → 线程向 Eden 申请一个新 TLAB;本质是从“可用 Eden Region 池”里拿/切一段;
    3. 若连新 TLAB 都拿不到(Eden 可用 Region 紧张),触发一次 Young GC
  • 超大对象(Humongous)
    • size ≥ 1/2 * regionSize,直接从 Free 列表里找一段连续 Region,标成 HumongousStart/Cont,对象按 Region 对齐落进去;
    • 找不到足够连续的 Region → 先做回收/整理(极端时 Full GC)。

小结:Mutator 的新对象只会进 Eden(或 Humongous),不会直接进 Old/Survivor。

GC 暂停中的“复制/晋升”分配

0) 背景:一次 Young/Mixed GC 的目标
  • 选定 CSet(Eden + 部分/全部 Survivor;Mixed 时再加若干 Old)。
  • 对 CSet 里的存活对象evacuation(复制式压缩),让这些 Region 变空回收掉。
1) 每个 GC 线程的“工具箱”
  • 工作队列:本线程待扫描对象;空了可work stealing
  • 两类目的地
    • Survivor:年轻代 to-space;
    • Old:老年代。
  • PLAB(Parallel/Promotion Local Allocation Buffer):每个线程对每个目的地有一个小的顺序分配缓冲(bump-pointer)。
    • 用完 → 向该目的地的当前目标 Region申请一个新的 PLAB 块;
    • 当前 Region 也用完 → 线程从 Free 列表一个新 Region作为该目的地的“当前 Region”(线程私有视角)。
  • GCAllocRegion/G1PLABAllocator:负责“给这个线程在 Survivor/Old 拿新 Region、切新 PLAB 块”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
evacuate(o):
// A. 并发安全的“是否已被别人复制过”
if (o.has_forwarding_pointer) return o.forward_to;

// B. 选择目的地(Survivor or Old)
age' = o.age + 1
if (age' < TenuringThreshold AND survivor_has_quota) dest = SURVIVOR
else dest = OLD

// C. 在本线程的 PLAB[dest] 分配
p = PLAB[dest].alloc(size(o))
if (p == null) {
// 1) 尝试从当前 Region 切出一个新的 PLAB
if (!PLAB[dest].refill(size(o))) {
// 2) 当前 Region 不够了 -> 申请一个新 Region 作为该目的地的“当前 Region”
if (!alloc_region_for(dest)) {
// 3) 目的地完全缺货(to-space exhausted)
//可能让其他线程多“收割”几个 CSet Region 以腾出 Free,或直接进入疏散失败路径
return evacuation_failure(o) // 见 §5
}
}
p = PLAB[dest].alloc(size(o)) // 重新分配
}

// D. 拷贝对象字节到 p,并在源对象头 CAS 安装转发表(forwarding ptr)
copy_bytes(o -> p)
if (!CAS(o.markWord, NULL, p)) { // 有别的线程抢先复制了
retire_or_reuse(p) // 回退这次分配
return o.forward_to
}

// E. 更新元数据
set_age(p, age')
push_fields_to_my_queue(p) // 子引用入本线程队列
record_survivor_age(age') // 用于下次自适应 TenuringThreshold
account_live_bytes(dest, size(o)) // 幸存字节统计

// F. 记忆集/卡表:p 中指向其他 Region 的引用,标脏卡(暂停内/外都会做一部分)
enqueue_dirty_cards_for_RSet(p)

return p
5) 疏散/晋升失败(Evacuation/Promotion Failure)

触发条件:目的地真的拿不到空间(to-space exhausted),或对象太“烫”导致反复失败。

  • 自指转发(self-forward):把源对象的转发表设为自己(即“留在原地”),标记所在 Region 为 pinned
  • 这样本次 GC 就不会再试图移动它,暂停后这些 Region 的碎片会增加;
  • 统计到“失败”后,G1 会增大 G1ReservePercent、放宽 MaxGCPauseMillis 或更积极进入 Mixed/并发标记,极端时可能触发 Full GC 兜底。
-------------本文结束,感谢您的阅读-------------