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
):- 先在 TLAB(线程本地分配缓冲)里走 bump-pointer;
- TLAB 不够 → 线程向 Eden 申请一个新 TLAB;本质是从“可用 Eden Region 池”里拿/切一段;
- 若连新 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 | evacuate(o): |
5) 疏散/晋升失败(Evacuation/Promotion Failure)
触发条件:目的地真的拿不到空间(to-space exhausted),或对象太“烫”导致反复失败。
- 自指转发(self-forward):把源对象的转发表设为自己(即“留在原地”),标记所在 Region 为 pinned;
- 这样本次 GC 就不会再试图移动它,暂停后这些 Region 的碎片会增加;
- 统计到“失败”后,G1 会增大
G1ReservePercent
、放宽MaxGCPauseMillis
或更积极进入 Mixed/并发标记,极端时可能触发 Full GC 兜底。