衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量和延迟。要在这三个方面同时具有卓越表现的“完美”收集器是极其困难甚至是不可能的,一款优秀的收集器通常最大可同时达成其中两项。
“延迟”的重要性日益凸显,越来越能容忍收集器多占用一点点内存。
第一款不由Oracle公司的虚拟机团队所领导开发的HotSpot垃圾收集器。
其更像是G1的下一代继承者。它们两者有着相似的堆内存布局,在初始标记、并发标记等许多阶段的处理思路上都高度一致,甚至还直接共享了一部分实现代码。
比G1至少有三个明显的不同之处。
能与用户线程并发。并发回收是Shenandoah与之前HotSpot中其他收集器的核心差异。并发回收困难点在于:在移动对象的同时,用户线程仍可能不停对被移动的对象进行读写访问。
图 并发回收阶段,用户线程与GC线程同时操作同一对象
即在并发回收阶段,用户线程对被复制对象的操作也需要在新复制的对象中体现。
不会有专门的新生代或者老年代Region的存在。并不是说分代对Shenandoah没有价值,这更多是出于性价比的权衡,基于工作量上的考虑而将其放到优先级较低的位置上。
降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题发生概率。连接矩阵可以理解为一张二维表格,如果Region N有对象指向Region M,就在表格的N行M列中打个标记。
Region 编号 |
1 |
2 |
3 |
4 |
1 |
||||
2 |
x |
x |
||
3 |
||||
4 |
x |
表 连接矩阵示意表
上表中的的含义为:Region 2中有对象指向Region 1和Region4,Region 4中有对象指向Region 1。
初始标记 |
与G1一样,首先标记与GC Roots直接关联的对象。 |
并发标记 |
与G1一样,标记对象图,标记出全部可达对对象。 |
最终标记 |
与G1一样,处理剩余SATB扫描,统计出回收价值最高的Region,将这些Region构成一组回收集。 |
并发清理 |
清理那些整个区域内连一个存活对象都没找到的Region。 |
并发回收 |
把回收集里的存活对象先复制一份到其他未被使用的Region中。 |
初始引用更新 |
实际上并未做什么具体的处理,设立这个过程只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器都已完成分配给它们的对象移动任务而已。 |
并发引用更新 |
真正开始进行引用更新操作,把堆中所有指向旧对象的引用修正到复制后到新地址。 |
最终引用更新 |
修正存在与GC Roots中的引用。 |
并发清理 |
回收没有存活对象的Region的内存空间。 |
表 Shenandoah收集器工作过程的九个阶段
Shenandoah用以支持并行整理的核心概念——Brooks Pointer(转发指针),是来实现对象移动与用户线程并行的一种解决方案。
此前,要实现对象移动与用户线程并行,通常是在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到归属于旧对象的内存空间就会产生自毁中断,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后到新对象上。
这种方法如果没有操作系统层面的直接支持,将导致用户态频繁切换到核心态,代价非常大,不能频繁使用。
在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向自己,否则指向复制后到新对象。
图 Brooks Pointers示意图
所有间接对象访问技术的缺点都是相同也是非常显著的:每次对象访问会带来一次额外的转向开销。
Brooks形式的转发指针在设计上决定了它必然会出现多线程竞争问题的,设想以下三件事情并发进行时的场景:
1)收集器线程复制了新的对象副本;
2)用户线程更新对象的某个字段;
3)收集器线程更换转发指针的引用值为新副本的地址。
如果不采取措施,事件2在事件1、事件3直接发生,将导致用户线程对对象的变更发生在旧对象上。
Shenandoah收集器通过比较并交换(CAS)操作来保证并发时对象的访问正确性。
图 Shenandoah CAS操作示意图
Z Garbage Collector,是一款在JDK11中新加入具有实验性质的低延迟垃圾处理器,是由Oracle公司研发。
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
ZGC的region具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有大、中、小三类容量:
小型Region |
容量固定为2MB,用于放置小于256KB的小对象。 |
中型Region |
容量固定为32MB,用于放置大等于256kb但小于4MB的对象。 |
大型Region |
容量不固定,但必须为2MB的整数倍,用于放置4MB或以上的大对象,大型Region在ZGC的实现中不会被重分配,因为复制一个大对象的代价非常昂贵。 |
表 ZGC的三类容量
之前,如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段,如对象的哈希码、分代年龄、锁记录等。这种记录方式如果在有对象访问的场景下是很自然流畅的,不会有额外的负担。
如果在不能保证对象访问能够成功,又或者有一些根本就不会去访问对象,但又希望得知该对象的某些信息(比如三色标记,这些标记本质上只和对象引用有关,和对象本身无关)的场景下,上面的记录方式就会产生额外的开销。
ZGC的染色指针直接把标识信息记在引用对象的指针上。
为什么指针本身可以存储额外信息?
操作系统不会全部使用指针位。
图 Linux 64位下的染色指针
不过染色指针的实现依赖操作系统和处理器的支持。
并发标记 |
与G1一样,是遍历对象图做可达性分析的阶段。与G1不同的是,其三色标记是在指针上而非对象。 |
并发预备重分配 |
需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。与G1不同,ZGC每次回收都会扫描所有的Region,用其成本换取省去G1中记忆集的维护成本。重分配集只是决定里面的存活对象会被重新复制到其他Region中,里面的Region会被释放。 |
并发重分配 |
把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。 |
并发重映射 |
修正整个堆中指向重分配集中旧对象的所有引用。 |
表 ZGC的运作过程
在并发重分配阶段,如果用户线程此时并发访问啦位于重分配集中的对象, 这次访问会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制到对象上,同时修正更新该引用到值,使其直接指向新对象。
ZGC将这种行为称为指针的“自愈”能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就只慢一次。