一、来源
- ZGC收集器是由Oracle公司研发的。2018年创建了JEP 333将ZGC提交给OpenJDK,推动其进入OpenJDK11的发布清单中。
二、ZGC的堆内存布局
- 与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局。
- ZGC的Region具有动态性。
- 动态创建和销毁
- 动态的区域容量大小
分类如下:
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,所以实际容量可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常高昂。
三、并发整理算法的实现。
3.1 算法的由来
- G1收集器的筛选回收阶段是stop the world的,但收集器线程间是并行的,之所以不和用户线程并发执行,是因为G1只回收一部分Region,停顿时间是用户可以控制的。所以并不着急去实现,交给了ZGC去实现。
- 并且因为G1为了不影响吞吐量才选择stw的。停顿用户线程可以最大幅度提高垃圾收集效率。
3.2 实现
3.2.1 读屏障
指针的自愈能力
- 在ZGC中,当读取处于重分配集的对象时,会被读屏障拦截,通过转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象。ZGC将这种行为叫做指针的“自愈能力”。
- 好处是:第一次访问旧对象访问会变慢,但也只会有一次变慢,当“自愈”完成后,后续访问就不会变慢了。
- Shenandoah每次访问都慢,对比发现,ZGC的执行负载更低。
3.2.2 染色指针技术
3.2.2.1 HotSpot虚拟机的标记实现方案有如下几种:
- 把标记直接记录在对象头上(如Serial收集器);
- 把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息);
- 直接把标记信息记在引用对象的指针上(如ZGC)
为什么会放在指针上呢?
- 追踪式收集算法的标记阶段就是看有没有引用,所以可以只和指针打交道而不管指针所引用的对象本身。
- 例如对象标记过程就是打个三色标记,这些标记本质上只和对象引用有关,和对象本身无关。某个对象只有它的引用关系才能决定它的存活。
3.2.2.2 染色指针的解释
- 染色指针是一种直接将少量额外的信息存储在指针上的技术。目前在Linux下64位的操作系统中高18位是不能用来寻址的,但是剩余的46为却可以支持64T的空间,到目前为止我们几乎还用不到这么多内存。于是ZGC将46位中的高4位取出,用来存储4个标志位,剩余的42位可以支持4TB(2的42次幂)的内存,也直接导致ZGC可以管理的内存不超过4TB,如图所示:
- 限制:只能在64位系统上,因为ZGC设置就是用的42-46位,32位明显不够嘛。。并且不支持压缩指针(这一块可以参考Java对象模型中的OOP,meta中有一个Klass直接指向Klass,还一个压缩指针)如下。
union _metadata {
之前都是oop,现在直接指向Klass了
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
3.2.2.3 染色指针的设计
ZGC使用了内存多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射。
因为染色指针只是重新定义内存中某些指针的其中几位,OS又不支持,OS只会把整个指针当做一个内存地址来对待,只是它自己瞎想,为了解决这个问题,使用了现代处理器的虚拟内存映射技术
-
现代处理器一般使用请求分页机制+虚拟内存映射技术。
- 请求分页机制把线性地址空间和物理地址空间分别划分为大小相等的块。这样的块称为页。通过在线性虚拟空间的页和物理地址空间的页建立映射表,分页机制会进行线性地址到物理地址的映射,完成线性地址到物理地址的转换。
- Linus/x86-64平台上的ZGC使用了多重映射将多个不同的虚拟内存地址映射到同一个物理内存地址上,多对一映射。意味着ZGC在虚拟内存空间中看到的地址空间比实际的堆内存容量更大。
-
把染色指针中的标志位看做是地址的分段符,只要把这些不同的地址段映射到同一个物理地址空间就行了,经过多重映射转换后,就可以使用染色指针正常进行寻址了。
- 标志位就是上图的Remapped,Marked1,Marked0。
3.2.2.4 染色指针的作用。
一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉
,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。而Shenandoah需要等到更新阶段结束才能释放回收集中的Region,如果Region里面对象都存活的时候,需要1:1的空间才能完成收集。染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量
,ZGC只使用了读屏障。因为信息直接维护在指针中。染色指针具备强大的扩展性,
它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据
,以便日后进一步提高性能。
四、ZGC的过程
并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记和最终标记也会出现短暂的停顿,整个标记阶段只会更新染色指针中的Marked 0、Marked 1标志位。
-
并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
- ZGC的重分配集只是决定里面的存活对象会被复制到其他的Region。不是为了效益回收
- JDK12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段完成的。
-
并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
- ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
- ZGC的染色指针因为“自愈”(Self-Healing)能力,所以只有第一次访问旧对象会变慢,而Shenandoah的Brooks转发指针是每次都会变慢。 一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉,因为可能还有访问在使用这个转发表。
并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。
五、ZGC的优点
- 低停顿,高吞吐量,ZGC收集过程中额外耗费的内存小
- G1通过写屏障维护记忆集,才能处理跨代指针,得以实现增量回收。记忆集占用大量内存,写屏障对正常程序造成额外负担。
- ZGC没有写屏障,卡表之类的。
- 在多核处理器的某种架构下,ZGC优先在线程当前所处的处理器的本地内存上分配对象,以保证内存高效访问。
六、ZGC的缺点
- 承受的对象分配速率不会太高,因为浮动垃圾。
- ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
- 造成回收到的内存空间小于期间并发产生的浮动垃圾所占的空间。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。
6.1 解决办法
- 增加堆容量大小,使得程序得到更多的喘息时间。治标不治本的方案。
- 从根本上解决这个问题,还是需要引入分代收集。让新生对象在一个专门区域创建,然后专门针对这个区域进行更频繁的,更快的收集。
参考资料
《深入理解Java虚拟机第三版》