卡表图解
分代垃圾收集与卡表机制详解
1. 分代垃圾收集概述 (Generational GC Overview)
内存堆结构示意
| 年轻代 (Young Gen) | 老年代 (Old Gen) | |
|---|---|---|
| 新对象 A | 旧对象 X | |
| 新对象 B | 旧对象 Y | |
| 新对象 C | 旧对象 Z |
解释:
分代垃圾收集器将堆内存划分为年轻代和老年代。新创建的对象通常在年轻代,由于它们生命周期短,年轻代会更频繁地进行垃圾收集(Minor GC)。长期存活的对象会被晋升到老年代,老年代的GC频率较低。
核心问题: 在进行只针对年轻代的Minor GC时,如何高效地识别出被老年代对象引用的年轻代对象?如果扫描所有老年代对象来查找这些引用,将抵消分代GC的性能优势。
2. 写屏障 (Write Barrier)
写屏障流程示意
| 旧对象 X |
|---|
| ↓ |
尝试执行:old_obj.field = young_obj; |
| 写屏障 (Write Barrier) |
|---|
| 拦截操作并检查: |
| 1. 赋值给旧对象成员变量? |
| 2. 新引用指向年轻对象? |
| ↓ |
| 如果满足条件,记录旧对象所在内存区域为“脏” |
解释:
写屏障是一段在程序修改引用类型成员变量时执行的代码。当一个老年代对象的成员变量被赋值以引用一个年轻代对象时,写屏障会被触发。它的作用是记录下这个“跨代引用”的事实,标记该老年代对象所在的内存区域为“脏”,表示该区域可能包含指向年轻代对象的引用。
3. 卡表 (Card Table)
老年代内存与卡表映射示意
| 老年代内存区域 (Old Gen Memory) | ------------> | 卡表 (Card Table Array) |
|---|---|---|
| 卡片 0 (512B) | 0 (干净) | |
| 卡片 1 (512B) | 1 (脏) | |
| (包含旧对象X) | 0 (干净) | |
| 卡片 2 (512B) | 0 (干净) | |
| 卡片 3 (512B) | ... | |
| ... | ... | |
| 卡片 N | ... | |
解释:
卡表是写屏障记录“脏”状态的方式,它是一种空间和时间上的权衡。它不是精确到每个对象,而是将老年代内存划分为固定大小的“卡片”(例如512字节或更小/大),并用一个位数组来表示这些卡片的状态。
如果一个卡片中包含的任何老年代对象引用了年轻代对象,则卡表中对应条目被标记为 '1' (脏);否则为 '0' (干净)。
卡表更新逻辑示例:
card_table[(&old_obj - start_of_heap) >> K] = 1;
其中 &old_obj 是旧对象的内存地址,start_of_heap 是堆的起始地址,K 是卡片大小的对数(例如,如果卡片是512字节,K=9,因为2^9=512)。这通过位运算高效地计算出旧对象所属卡片的索引。
4. 次要GC过程 (Minor GC Process)
Minor GC流程步骤
- 扫描活跃根 (GC Roots) 和年轻代内部的引用,标记可达的年轻代对象。
↓ - 扫描卡表,只查找标记为 '1' (脏) 的卡片。
↓ - 仅扫描这些“脏”卡片对应的老年代内存区域,从中发现对年轻代对象的引用,并标记这些年轻代对象。
↓ - 清除所有未被标记(即不可达)的年轻代对象。
解释:
在Minor GC执行时,垃圾收集器不再需要扫描整个老年代。它首先处理GC根和年轻代内部的引用。然后,它利用卡表,只检查那些被写屏障标记为“脏”的卡片。这意味着GC只需要扫描老年代中很小一部分区域来查找跨代引用,从而大大减少了GC的暂停时间,提高了效率。
GC扫描卡表伪代码:
for i from 0 to (heap_size >> K):
if card_table[i]:
scan heap[i << K .. (i + 1) << K] for young pointers

浙公网安备 33010602011771号