乐趣区

关于后端:垃圾收集器必问系列ZGC

本文已收录至 Github,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

CSDN:码农 BookSea

人的所有苦楚,实质上都是对本人的能干的愤恨。——王小波

ZGC 有人称它为 Zero GC,其实“Z”并非什么专业名词的缩写,这款收集器的名字就叫作 Z Garbage Collector。依据 OpenJDK 官方网站的阐明 ZGC 其实并没有什么非凡意义,就是一个名字而已。起初只是为了致敬 ZFS 文件系统,示意 ZGC 与 ZFS 一样都是革命性的,是一个跨时代的产品。更像是一种崇拜命名法。所以 ZGC 就是要做革命性的与以往的垃圾回收器性能上有很大进步的 GC。

ZGC 的指标是心愿在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都能够把垃圾收集的进展工夫限度在十毫秒以内的低提早。

在 ZGC 算法中,并没有分代的概念,所以就不存在 Young GC、Old GC,所有的 GC 行为都是 Full GC。

Region 布局

先从 ZGC 的内存布局说起。 和 G1 一样,ZGC 也采纳基于 Region 的堆内存布局,但与 G1 不同的是,ZGC 的 Region 具备动态性——动态创建和销毁,以及动静的区域容量大小 。在 x64 硬件平台下,ZGC 的 Region 能够有小、中、大、三类容量:

  • 小型 Region(Small Region):容量固定为 2MB,用于搁置小于 256KB 的小对象。
  • 中型 Region(Medium Region):容量固定为 32MB,用于搁置大于等于 256KB 但小于 4MB 的对象。
  • 大型 Region(Large Region):容量不固定,能够动态变化,但必须为 2MB 的整数倍,用于搁置 4MB 或以上的大对象。每个大型 Region 中只会寄存一个大对象,这也预示着尽管名字叫作“大型 Region”,但它的理论容量齐全有可能小于中型 Region,最小容量可低至 4MB。大型 Region 在 ZGC 的实现中是不会被重调配的,因为复制一个大对象的代价十分昂扬。

读屏障

之前的 GC 都是采纳写屏障(Write Barrier),而 ZGC 采纳的是读屏障,读屏障(Load Barriers)相似于 Spring AOP 的前置告诉。在 ZGC 中,当读取处于重调配集的对象时,会被读屏障拦挡,通过转发表记录将拜访转发到新复制的对象上,并同时修改更新该援用的值,使其间接指向新对象,ZGC 将这种行为叫做指针的“ 自愈能力 ”。这样就算 GC 把对象挪动了,读屏障也会发现并修改指针,于是利用代码就永远都会持有更新后的无效指针,而且不须要 STW,相似 JDK 里的 CAS 自旋,读取的值发现曾经生效了,须要从新读取。

益处是:第一次拜访旧对象拜访会变慢,但也只会有一次变慢,当“自愈”实现后,后续拜访就不会变慢了

正是因为 Load Barriers 的存在,所以会导致配置 ZGC 的利用的吞吐量会变低。不过这点开销是值得的。

染色指针

ZGC 收集器有一个标志性的设计是它采纳的染色指针技术。

ZGC 呈现之前,GC 信息保留在对象头的 Mark Word 中,如对象的哈希码、分代年龄、锁记录等就是这样存储的。

追踪式收集算法的标记阶段就可能存在只跟指针打交道而不用波及指针所援用的对象自身的场景。例如对象标记的过程中须要给对象打上三色标记,这些标记实质上就只和对象的援用无关,而与对象自身无关、ZGC 的染色指针将这些信息间接标记在援用对象的指针上

染色指针是一种间接将大量额定的信息存储在指针上的技术,Linux 下 64 位指针的高 18 位不能用来寻址,ZGC 的染色指针技术盯上了这剩下的 46 位指针宽度, 将其高 4 位提取进去存储四个标记信息 。当然,因为这些标记位进一步压缩了本来就只有 46 位的地址空间,也间接导致 ZGC 可能治理的内存不能够超过 4TB(2 的 42 次幂)

JVM 能够从指针上间接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重调配集(Remapped)、是否须要通过 finalize 办法来拜访到(Finalizable)。

18 位:预留给当前应用;1 位:Finalizable 标识,此位与并发援用解决无关,它示意这个对象只能通过 finalizer 能力拜访;1 位:Remapped 标识,设置此位的值后,对象未指向 relocation set 中(relocation set 示意须要 GC 的 Region 汇合);1 位:Marked1 标识;1 位:Marked0 标识,和下面的 Marked1 都是标记对象用于辅助 GC;42 位:对象的地址(所以它能够反对 2^42=4T 内存);

染色指针的劣势

染色指针次要有三大劣势:

  • 染色指针能够使得一旦某个 Region 的存活对象被移走之后,这个 Region 立刻就可能被开释和重用掉,而不用期待整个堆中所有指向该 Region 的援用都被修改后能力清理 。实践上只有还有一个闲暇 Region,ZGC 就能实现收集。
  • 染色指针能够大幅缩小在垃圾收集过程中内存屏障的应用数量 ,ZGC 只应用了读屏障。因为信息间接保护在指针中。
  • 染色指针能够作为一种可扩大的存储构造用来记录更多与对象标记、重定位过程相干的数据,以便日后进一步提高性能 。如果开发了前 18 位指针,既能够腾出已用的 4 个标记位,将 ZGC 可反对的最大堆内存从 4TB 拓展到 64TB,也能够利用其余地位再存储更多的标记,譬如存储一些追踪信息来让垃圾收集器在挪动对象时能将低频次应用的对象挪动到不常拜访的内存区域。

运作过程

ZGC 的运作过程大抵可划分为以下四个大的阶段。 全部四个阶段都是能够并发执行的,仅是两个阶段两头会存在短暂的进展小阶段 ,这些小阶段,譬如初始化 GC Root 间接关联对象的 Mark Start,ZGC 的运作过程具体如图所示。

  • 并发标记(Concurrent Mark):并发标记是遍历对象图做可达性剖析的阶段。与 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 很奇妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去实现,反正它们都是要遍历所有对象的,这样合并就节俭了一次遍历对象图的开销。一旦所有指针都被修改之后,原来记录新旧对象关系的转发表就能够开释掉了。

ZGC 简直整个收集过程都全程可并发,短暂进展也只与 GC Roots 大小相干而与堆内存大小无关,因此同样实现了任何堆上进展都小于十毫秒的指标

ZGC 的优缺点

相比 G1、Shenandoah 等先进的垃圾收集器,ZGC 在实现细节上做了一些不同的衡量抉择,譬如 G1 须要通过写屏障来保护记忆集,能力解决跨代指针,得以实现 Region 的增量回收。记忆集要占用大量的内存空间,写屏障也对失常程序运行造成额外负担,这些都是衡量抉择的代价。ZGC 就齐全没有应用记忆集,它甚至连分代都没有,连像 CMS 中那样只记录新生代和老年代间援用的卡表也不须要,因此齐全没有用到写屏障,所以给用户线程带来的运行累赘也要小得多

可是,有优就有劣,ZGC 的这种抉择也限度了它能接受的对象调配速率不会太高,因为 ZGC 四个阶段都反对并发,如果调配速率高,将发明大量的新对象,这就产生了大量的浮动垃圾。如果这种高速调配继续维持的话,回收到的内存空间继续小于期间并发产生的浮动垃圾所占的空间,堆中残余可腾挪的空间就越来越小了。目前惟一的方法就是尽可能地减少堆容量大小,取得更多喘息的工夫。 然而若要从根本上晋升 ZGC 可能应答的对象调配速率,还是须要引入分代收集,让新生对象都在一个专门的区域中创立。所以分代算法有利有弊。


如果本篇博客有任何谬误和倡议,欢送给我留言斧正。文章继续更新,能够关注公众号第一工夫浏览。

退出移动版