卡表图解

分代垃圾收集与卡表机制详解

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流程步骤

  1. 扫描活跃根 (GC Roots) 和年轻代内部的引用,标记可达的年轻代对象。
  2. 扫描卡表,只查找标记为 '1' (脏) 的卡片。
  3. 仅扫描这些“脏”卡片对应的老年代内存区域,从中发现对年轻代对象的引用,并标记这些年轻代对象。
  4. 清除所有未被标记(即不可达)的年轻代对象。

解释:
在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
posted @ 2025-08-29 16:20  zyz学习探索  阅读(40)  评论(0)    收藏  举报