1)内存布局

小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。

中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于 4MB的对象。

大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于 放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫 作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型 Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收 集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。

2)染色指针

染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储 额外信息呢?在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节 [3] 。实际上,基于需求(用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多) 和成本(消耗更多晶体管)的考虑,在AMD64架构 [4] 中只支持到52位(4PB)的地址总线 和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存只有 256TB。此外,操作系统一侧也还会施加自己的约束,64位的Linux则分别支持47位 (128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统甚 至只支持44位(16TB)的物理地址空间。

尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今 天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46 位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指 针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过 finalize()方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有46位的地址空 间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂) 。

3)收集过程

并发标记 (Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达 性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的 名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、 Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色 指针中的Marked 0、Marked 1标志位。

并发预备重分配 (Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件 统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的 目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在 JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。

并发重分配 (Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把 重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表 (Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集 器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位 于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发 表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象, ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问 旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次对象访 问都必须付出的固定开销,简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比 Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集 中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但 是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系, 这些旧指针一旦被使用,它们都是可以自愈的。

并发重映射 (Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对 象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的 并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可 以自愈的,最多只是第一次 使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结 束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并 发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它 们都是要遍历所有对象的,这样合并就节省了一次遍历对象图 [9] 的开销。一旦所有指针都被 修正之后,原来记录新旧对象关系的转发表就可以释放掉了。