Pauseless GC:挑战无暂停的垃圾回收

97 阅读4分钟

ZGC 和 G1 有很多相似的地方,它的主体思想也是采用复制活跃对象的方式来回收内存。在回收策略上,它也同样将内存分成若干个区域,回收时也会选择性地先回收部分区域。

ZGC 与 G1 的区别在于:它可以做到并发转移(拷贝)对象,并发转移指的是在对象拷贝的过程中,应用线程和 GC 线程可以同时进行,这是其他 GC 算法目前没有办法做到的。

ZGC 提升效率的核心关键在于并发转移阶段使用了 read barrier。如果 GC 线程没有搬移完成,那么应用线程可以去读这个对象的旧地址;如果这个对象已经搬移完成,那么可以去读这个对象的新地址。那么判断这个对象是否搬移完成的动作就可以由 read barrier 来完成。

为了达到这个目的,ZGC 采用了用空间换时间的做法,也就是染色指针(colored pointer)技术。

染色指针

在 64 位系统下,当前 Linux 系统上的地址指针只用到了 48 位,寻址范围也就是 256T。ZGC 借用了地址的第 42 ~ 45 位作为标记位,第 0 ~ 41 位共 4T 的地址空间留做堆使用。

第 42-45 这 4 位是标记位,它将地址划分为 Marked0、Marked1、Remapped、Finalizable 四个地址视图。

地址视图的巧妙之处就在于,一个在物理内存上存放的对象,被映射在了三个虚拟地址上。

ZGC 的回收原理

image.png

Pauseless 的三个核心步骤分别是:Mark、Relocate 和 Remap。

Mark

事实上,ZGC 也不是完全没有 STW 的。在进行初始标记时,它也需要进行短暂的 STW。不过在这个阶段,ZGC 只会扫描 root,之后的标记工作是并发的,所以整个初始标记阶段停顿时间很短。也正是因为这一点,ZGC 的最大停顿时间是可控的,也就是说停顿时间不会随着堆的增大而增加。

初始标记工作完成之后,就可以根据 root 集合进行并发标记了。

在 GC 开始之前,地址视图是 Remapped。那么在 Mark 阶段需要做的事情是,将遍历到的对象地址视图变成 Marked0,也就是修改地址的第 42 位为 1。前面我们讲过,三个地址视图映射的物理内存是相同的,所以修改地址视图不会影响对象的访问。

除此之外,应用线程在并发标记的过程中也会产生新的对象。类似于 G1 中的 SATB 机制,新分配的对象都认为是活的,它们地址视图也都标记为 Marked0。至此,所有标记为 Marked0 的对象都认为是活跃对象,活跃对象会被记录在一张活跃表中。

而视图仍旧是 Remapped 的对象,就认为是垃圾。接下来,我们进入 Relocate 阶段,也就是转移阶段。

Relocate

Relocate 阶段的主要任务是搬移对象,在经过 Mark 阶段之后,活跃对象的视图为 Marked0。搬移工作要做两件事情:

  1. 选择一块区域,将其中的活跃对象搬移到另一个区域;
  2. 将搬移的对象放到 forwarding table。

ZGC 是分块的,块区域叫 Page。

forwarding table,它是一张维护对象搬移前和搬移后地址的映射表,key 是对象的旧地址,value 是对象的新地址。

当 GC 线程遍历到一个对象,如果对象地址视图是 Marked0,就将其转移,同时将地址视图置为 Remapped,并加入到 forwarding table ;如果访问到一个对象地址视图已经是 Remapped,就说明已经被转移了,也就不做处理了。

Remap

Remap 阶段主要是对地址视图和对象之间的引用关系做修正。

在 Remap 阶段,新分配对象的地址视图是 Marked1,如果遇到对象地址视图是 Marked0 或者 Remaped,就把地址视图置为 Marked1。

ZGC 的回收过程大致分为三个主要阶段,其中 Mark 阶段负责标记活跃对象、Relocate 阶段负责活跃对象转移、ReMap 阶段负责地址视图统一。因为 Remap 阶段也需要进行全局对象扫描,所以 Remap 和 Mark 阶段是重叠进行的。


此文章为7月Day23学习笔记,内容来源于极客时间《编程高手必学的内存知识》