关于后端:垃圾收集器必问系列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可能应答的对象调配速率,还是须要引入分代收集,让新生对象都在一个专门的区域中创立。所以分代算法有利有弊。


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

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据