ZGC(Z Garbage Collector)

1.ZGC收集器

zgc在希望对吞吐量尽量不影响的情况下,降低堆内存垃圾收集延迟的收集器。在jdk11时zgc就被引入Java,并在jdk13中对zgc做了增强(ZGC 可以将未使用的堆内存返回给操作系统)

ZGC 收集器是基于Region内存布局的,不设分代 的, 使用了读屏障、 染色指针和内存多重映射等技术来实现可并发的标记- 整理 算法 的, 以低延迟为首要目标的 一 款 垃圾 收集 器。

2.内存布局

ZGC基于Region的堆内存布局, 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 的 实现中是不会被重分配(重分配是ZGC的一种处理动作, 用于复制 对象 的 收集 器 阶段) 的, 因为复制一个大对象的代价非常高昂。

3.读屏障

读屏障可以看作在虚拟机层面 对“引用类型字段读取” 这个动作的AOP切面,在引用对象读取时会产生一个环形通知,供程序执行额外的动作, 也就是说读取的前后都在写屏障的覆盖范畴内。

4.染色指针

从前,我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段《Java的对象头》,如对象的哈希码、分代年龄、锁记录等就是这样存储的。这种记录方式在有对象访问的场景下是很自然流畅的,不会有什么额外负担。但如果对象存在被移动过的可能性,即不能保证对象访问能够成功呢?又或者有一些根本就不会去访问对象,但又希望得知该对象的某些信息的应用场景呢?

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

ZGC(Z Garbage Collector)_第1张图片

  • Linux下64位指针的高18位不能用来寻址,所有不能使用;
  • Finalizable:表示是否只能通过finalize()方法才能被访问到,其他途径不行;
  • Remapped:表示是否进入了重分配集(即被移动过);
  • Marked1、Marked0:表示对象的三色标记状态;
  • 最后42用来存对象地址,最大支持4T;

5.ZGC运行过程

ZGC(Z Garbage Collector)_第2张图片

  • 并发标记(ConcurrentMark):并发标记是遍历对象图做可达性分析的阶段,前后也要经过初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked0、Marked1标志位。
  • 并发预备重分配(ConcurrentPrepareforRelocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(RelocationSet)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
  • 并发重分配(ConcurrentRelocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(ForwardTable),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。

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

6.ZGC的优势和缺点

  • 染色指针有4TB的内存限制,不能支持32位平台,不能支持压缩指针(-XX:+UseCompressedOops)等诸多约束。
  • ZGC准备要对一个很大的堆做一次完整的并发收集,假设其全过程要持续数分钟以上,在这段时间里面,由于应用的对象分配速率很高,将创造大量的新对象,这些新对象很难进入当次收集的标记范围,通常就只能全部当作存活对象来看待——尽管其中绝大部分对象都是朝生夕灭的,这就产生了产生了大量的浮动垃圾。如果这种高速分配持续维持的话,每一次完整的并发收集周期都会很长,回收到的内存空间持续小于期间并发产生的浮动垃圾所占的空间,堆中剩余可腾挪的空间就越来越小了。
  • 染色指针有一个必须解决的前置问题:Java虚拟机作为一个普普通通的进程,这样随意重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持?
  • 收集过程中额外耗费的内存小,染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。使得理论上只要还有一个空闲Region,ZGC就能完成收集。
  • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。实际上,到目前为止ZGC都并未使用任何写屏障,只使用了读屏障(一部分是染色指针的功劳,一部分是ZGC现在还不支持分代收集,天然就没有跨代引用的问题)。
  • 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。现在Linux下的64位指针还有前18位并未使用,它们虽然不能用来寻址,却可以通过其他手段用于信息记录。如果开发了这18位,既可以腾出已用的4个标志位,将ZGC可支持的最大堆内存从4TB拓展到64TB,也可以利用其余位置再存储更多的标志,譬如存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。
     

你可能感兴趣的:(JVM)