ZGC

概述

ZGC(Z Garbage Collector),在 jdk 11 中引入的一种可扩展的低延迟垃圾收集器,在 jdk 15 中发布稳定版。

设计目标:

ZGC特征

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

内存布局

ZGC内存布局与G1一样,均采用了基于Region的堆内存布局,但ZGC的Region具有动态性(动态创建和销毁,以及动态的区域容量大小)。在x64的硬件平台下,ZGC的Region可以具有大、中、小三类容量。

染色指针(Colored Pointer)

Colored Pointer,即染色指针,是ZGC的核心设计之一,如下图所示。以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中(直接将标记信息记录在对象的引用指针上)。

image.png

每个对象有一个64位指针,这64位被分为以下:

内存多重映射

ZGC使用了多重映射技术,将多个不同的虚拟内存地址映射到同一个物理内存地址上,是一种多对一映射。
image.png

当应用程序创建对象时,会在堆上申请一个虚拟地址,这时ZGC会为这个对象在M0,M1和Remapped这三个视图空间分别申请一个虚拟地址,这三个虚拟地址映射到同一个物理地址。

M0、M1和Remapped视图都可以映射到实际的物理地址,但在ZGC中,同一时间仅有一个视图空间有效。由ZGC并发过程演示可以知道,这三个视图空间的切换是由垃圾回收的不同阶段触发的,通过限定三个空间在同一时间仅有一个空间有效,利用虚拟空间换时间,高效地完成GC过程中的并发操作。

读屏障(Load Barriers)

ZGC为了解决内存碎片化的问题引入了Relocation,但对于一个很大的堆来说,Relocation的过程相当缓慢,因为ZGC并不希望有较大的延时,因此会将大多数的Relocation过程与应用程序并发执行。但是这就引入了另外一个问题,例如当前有一个线程的引用,然后ZGC Relocation了这个对象的引用,紧接着发生了线程的上下文切换,用户线程尝试获取这个对象的旧内存地址,那么此时肯定是无法获取到的。

ZGC引入了读屏障(Load Barriers)来解决这个问题,Load Barriers是线程从堆中获取一个对象引用时加入的一小段代码(仅“从堆中读取对象引用”才会触发这段代码)。

读屏障作用:在程序需要并行获取对象的引用时,ZGC就会对该对象的指针进行读取,判断Remapped标识,如果标识为该对象位于本次需要清理的Region区中,该对象则会有内存地址变化,会在指针中将新的引用地址替换掉原有对象的引用地址,然后再进行返回。

Object O = obj.fieldA;      //从堆中获取引用,需要加入读屏障
<load barriers needed here>
Object p = o;               //无需加入读屏障,因为不是从堆中读取引用
o.doSomething();						//无需加入读屏障,因为不是从堆中读取引用
int i = obj.fieldB;         //无需加入读屏障,因为不是对象引用

ZGC工作原理

下图展示了ZGC三个暂停阶段(STW):Pause Mark Start(初始标记)、Pause Mark End(再次标记)、Pause Relocate Start(初始转移)

**Pause Mark Start(初始标记)和Pause Relocate Start(初始转移)**都只需要扫描所有GC Roots,其处理时间和GC Roots的数量是成正比的,一般耗时较短,**Pause Mark End(再次标记)**阶段重新标记并发标记阶段发生变化的对象,还会对非强引用(软引用,虚引用等)进行并行标记,该阶段标记的对象少,耗时很短,超过1ms则再次进入并发标记阶段。
image.png

ZGC的主要运作过程可以分为以下四个阶段

ZGC并发处理演示

上面提到过并发重映射是合并在下一次标记阶段的,因此在标记阶段其实存在两个地址视图M0和M1,之所以存在两个地址视图,就是为了区别前一次标记和当前标记,也就是说,当第二次进入到并发标记阶段后,地址视图调整为M1,而非M0。

第二次GC时M0的视图的对象代表的是上一次GC中被标记为活跃的对象,但并未被转移,且本次GC过程中被未被标记为活跃的对象。M1视图的对象代表的是本次GC过程中被标记为活跃的对象。Remapped视图是上次垃圾回收发生转移或者被Java应用程序访问过的对象,本次垃圾回收中被未被标记为活跃的对象。

ZGC核心参数

参数

使用样例

说明

-XX:+UseZGC

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

启用ZGC

-Xmx

-Xms10G -Xmx10G

设置最大堆内存

-Xlog:gc

打印GC日志

-Xlog:gc*

打印GC详细日志

-XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize

-XX:ReservedCodeCacheSize=64m

-XX:InitialCodeCacheSize=128m

设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m就已经足够

-XX:ConcGCThreads

-XX:ConcGCThreads=2

并发垃圾回收时线程数,默认为总核数的12.5%

-XX:ParallelGCThreads

-XX:ParallelGCThreads=6

STW阶段使用的线程数,默认是总核数的60%

-XX:ZCollectionInterval

-XX:ZCollectionInterval=120

ZGC发生的最小时间间隔,单位秒

-XX:ZAllocationSpikeTolerance

-XX:ZAllocationSpikeTolerance=5

ZGC触发自适应算法的修正系数,默认为2,数值越大越早触发ZGC

-XX:+UnlockDiagnosticVMOptions

-XX:-ZProactive

-XX:+UnlockDiagnosticVMOptions

-XX:-ZProactive

是否启用主动回收

触发时机

ZGC日志分析

public class Main {
    /**
     * VM options:-XX:+UseZGC -Xmx16m -Xlog:gc*
     * @param args
     */
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[2048]);
        }
    }
}

GC日志中每一行都注明了GC过程中的信息,关键信息如下:

总结

内存多重映射和染色指针的引入,使 ZGC 的并发性能大幅度提升。

ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有 GC Roots,STW 时间 GC Roots 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。可见整个 ZGC 的 STW 时间几乎只跟 GC Roots 数量有关系,不会随着堆大小和对象数量的变化而变化。

ZGC 也有一个缺点,就是浮动垃圾。因为 ZGC 没有分代概念,虽然 ZGC 的 STW 时间在 1ms 以内,但是 ZGC 的整个执行过程耗时还是挺长的。在这个过程中 Java 线程可能会创建大量的新对象,这些对象会成为浮动垃圾,只能等下次 GC 的时候进行回收。

参考文献

新一代垃圾回收器ZGC的探索与实践

ZGC垃圾收集器

ZGC原理与实现分析

mmap原理简介

https://www.usenix.org/legacy/events/vee05/full_papers/p46-click.pdf