ZGC笔记

ZGC的内存以page划分,有小、中、大,三类page,算法执行主要分为三个阶段:Mark、Relocate、Remap,通过染色指针标记,算法的基础是标记-复制。 不分代回收,算法的设计目标是可扩展的低延迟,目前仅支持64位系统。

概述

指针64位,但是ZGC只用低42位寻址,也就是支持4T的内存,指针的其他部分,有四位用于标记(42~45)

1
2
3
4

 63 ... 46   45 44 43 42 41 ... 0
|0 ...  0  | 0  0  0  0 | 0 ... 0 |
             F  R  M1 M0

四位,其中一次只有一位会置1,所以下文描述,标记为M0,即表示M0置1,即0001

程序进入垃圾回收,进入安全区后。

第一阶段Mark

进行初始标记(stop the world,STW),得到所有根节点的refs(引用), 所有根指向对象的所有引用成员的地址全部标为M0,这些refs全放到条带集(可以理解成一个集合)。

进行并发标记(非STW),取出条带集的refs,沿着refs可达到的指针,进行递归,将指针全部标为M0。 在这个过程中,应用线程创建的对象的指针全部为M0, 即这个阶段创建的对象全部都是活跃的,不参与垃圾回收。

最重要的一个问题是,应用线程并发执行,可能干扰标记过程,这里使用SATB,即快照,比如一开始,a.b=v,应用线程执行了a.b=v’, 快照就是要保证能得到原始的引用关系,即v会被加到条带集中进行标记,使用的是写屏障(write barrier)

第二阶段Relocate

指针全部标记完成,进入Relocate,开始回收内存了,回收的形式是复制,也就是把page中所有标为M0的对象搬到另一块page, 然后回收掉旧page即可,但往往对象数量很多,所以处理一部分page,即page的回收集。挑哪些page回收呢?

标记过程,除了标记指针,也会记录page中哪些被标为M0对象,记在位图里,page中的每个对象,在位图里代表一位,标指针的时候,也将对应对象的位置1, 然后还会记录对象的大小,这样就能计算复制到另一page的成本,置1对象总的占的空间越大,成本越高。

选完回收集,然后遍历回收集中的page,遍历page的位图,将置1的对象复制到新page上,并登记转移表(A地址转移到A’)

回收集里的page,标M0的对象全部复制以后,这些page也都释放了。 这个过程也是并发执行的,page要是释放了,应用程序如果访问了回收集page的对象,怎么办?会触发读屏障(read barrier),如果指针是标记M0的, 那就访问转移表,得到新的地址,将该地址置位R(表示Remapped,已转移),并回写(这样下一次就不用查了)。如果指针的标记就是R,说明是转移过的了,正常访问即可。 这个过程叫做指针的自愈

这个阶段的对象分配的指针全是标记R的,这样就能正常访问,所以第一阶段新分配的地址是M0,第二阶段是R,中间其实还有短暂的STW。

第三阶段Remap

这样回收集里的page全部释放后,现在的情况是还有很多对象的引用依然指向这些释放的地址,所以转移表要继续留下来,便于调整全部对象的引用关系。

另外,现在所有指针有两种情况:

  • 有些是M0的:说明是第一阶段新分配的对象的指针,或者是第一阶段被标记的
  • 有些是R的:说明是第二阶段新分配的对象的指针,或者是进行应用程序读取转移表后调整的指针,或者是第一阶段中的垃圾对象(不在回收集page里的)

接下来要调整所有引用的地址,全部迁到转移后的地址上,要遍历所有的对象,才能保证地址全部调整过来。

Mark做可达性标记也是要遍历所有对象,所以,将第一轮的Remap和第二轮的Mark放到一起做,用标记M1。

首先,新分配对象的地址是标记M1, 然后,遍历对象,如果遇到指针标记为M0,对象地址在转移表里,回写新地址,置位M1;遇到R,置位M1

和原先描述一样,也是并发执行的,靠指针自愈,应用线程也能正常工作。

全部结束后,也就完成了全部指针的调整,以及完成了新一轮的Mark。

遗留的问题

本文的描述是一个简版,具体实现由很多细节,实现中甚至有十个步骤。 暂时先列一些问题。

  1. 标记过程中,条带集满了怎么办?
  2. 复制过程和转移表写入的原子性?是否要等待两件事同时完成?
  3. 何时清理转移表?转移表规模会不会很大?
  4. 读/写屏障是怎么实现?