记忆集
:
是一种用于记录
从非收集区域指向收集区域的指针集合的抽象数据结构
。如果我们不考虑
效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结
构
记忆集作用
:解决对象跨代引用所带来的问题,垃圾收集器在新生代中建
立了名为记忆集(
Remembered Set
)的数据结构,用以避免把整个老年代加进
GC Roots
扫描范围。事
实上并不只是新生代、老年代之间才有
跨代引用的问题
。所有涉及部分区域收集(
Partial GC
)行为的
垃圾收集器,典型的如
G1
、
ZGC
和
Shenandoah
收集器,都会面临相同的问题,因此我们有必要进一步
理清记忆集的原理和实现方式
这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾
收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针
就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为
粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范
围以外的)的记录精度:
·
字长精度:
每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的
32
位或
64
位,这个
精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
·
对象精度:
每个记录精确到一个对象,该对象里有字段含有跨代指针。
·
卡精度:
每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
“
卡精度
”
所指的是用一种称为
“
卡表
”
(
Card Table)的方式去实现记忆集,
就是
记忆集的一种具体实现
,它定义了记忆集的记录精度、与堆内存的映射关系等。
关于卡表与记忆集的关系,读者不妨按照
Java
语言中
HashMap
与
Map
的关系来类比理解
卡表最简单的形式可以只是一个字节数组
,而
HotSpot
虚拟机确实也是这样做的。以下这行代
码是
HotSpot
默认的卡表标记逻辑
:
CARD_TABLE [this address >> 9] = 0;
字节数组
CARD_TABLE
的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个
内存块被称作
“
卡页
”
(
Card Page
)。一般来说,卡页大小都是以
2的N次幂
的字节数,通过上面代码可
以看出
HotSpot
中使用的卡页是
2
的
9
次幂,即
512
字节(地址右移
9
位,相当于用地址除以
512
)。那如
果卡表标识内存区域的起始地址是
0x0000
的话,数组
CARD_TABLE
的第
0
、
1
、
2
号元素,分别对应了
地址范围为
0x0000
~
0x01FF
、
0x0200
~
0x03FF
、
0x0400
~
0x05FF
的卡页内存块
,如图
所示。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代
指针,那就将对应卡表的数组元素的值标识为
1
,称为这个
元素变脏
(
Dirty
),没有则标识为
0
。在垃圾收集发生时,只要
筛选出卡表中变脏的元素
,就能轻易得出哪些卡页内存块中包含跨代指针,把它
们加入
GC Roots
中一并扫描