主页 新一代 Java垃圾回收神器:ZGC
文章
取消

新一代 Java垃圾回收神器:ZGC

你好,我是猿java。

今天我们分享的内容是:新一代 Java垃圾回收神器:ZGC。

ZGC 定义

ZGC(The Z Garbage Collector),是一种可扩展的低延迟垃圾收集器,主要是用来处理超大内存(TB级别)的垃圾回收。 ZGC 最初是 JDK 11 以一项实验性功能引入的,经过几个版本的迭代,最终在 JDK 15中被宣布为 Production Ready。

ZGC的中的”Z”代表什么含义?官方解释如下:

img.png

大概意思为:Z它不代表任何东西,ZGC只是一个名字。它最初受到 ZFS(文件系统)的启发或致敬,ZFS(文件系统)在首次问世时在许多方面都是革命性的。最初,ZFS 是“Zettabyte File System”的首字母缩写词,但这个意思被废弃了,后来据说它不代表任何东西。这只是一个名字。有关详细信息,请参阅Jeff Bonwick 的博客

下图为 Oracle官方对 ZGC停顿时间的描述:

img.png

ZGC的目标

2018年,Oracle官方描述的 ZGC目标为:

  1. 处理 TB 量级的堆;
  2. GC 时间不超过 10ms;
  3. 相对于使用 G1,应用吞吐量的降低不超过 15%;

img.png

2022年11月,Oracle官方描述的 ZGC的目标为:

  1. 低延时,亚毫秒的最大暂停时间
  2. 暂停时间不会随着堆、live-set 或 root-set 的大小而增加
  3. 处理 TB 量级的堆(可以处理 8MB-16TB 的堆);

img.png

通过官方对 ZGC目标的描述也可以看出,在几年的时间内,ZGC垃圾回收器的最大停顿时间已从 10ms 降低到 亚毫秒 级别,性能有了质的飞越。

核心技术点

2018年,官方描述的 ZGC的核心技术点为:

  • Concurrent:并发
  • Tracing: 可追踪
  • Compacting:整理内存
  • Single generation: 单代,也就是不进行分代
  • Region-based:基于Region
  • NUMA-aware:支持NUMA,Non-Uniform Memory Access(非一致内存访问)
  • Load barriers:读屏障
  • Colored pointers:染色指针,一种将信息存储在指针中的技术

img.png

2022年,官方描述的 ZGC的核心技术点为:

  • Concurrent:并发
  • Region-based:基于Region
  • Compacting:整理内存
  • NUMA-aware:支持NUMA,Non-Uniform Memory Access(非一致内存访问)
  • Using colored pointers:使用染色指针,一种将信息存储在指针中的技术
  • Using load barriers:使用读屏障

img.png

比较 2018年 和 2022年官方对 ZGC核心技术的描述可以发现,官方把 Single generation 这一点去掉了,熟悉 G1的小伙伴应该知道,G1是 Region-based类型,每个 Region在同一时刻只能属于一种分代,但是,一个Region可以在多个分代之间动态切换,因此,ZGC 从 最初的不分代发展成和 G1一样,也有分代。

ZGC 原理

Region-based

Region-based:基于区域。

ZGC 和 G1等垃圾回收器一样,也会将堆划分成很多的小分区,整个堆内存分区如下图:

img.png

ZGC的 Region 有小、中、大三种类型:

  • Small Region(小型 Region):容量固定为 2M, 存放小于 256K的对象;
  • Medium Region(中型 Region):容量固定为 32M,放置大于等于 256K,并小于 4M的对象;
  • Large Region(大型 Region): 容量不固定,可以动态变化,但必须为 2M的整数倍,用于放置大于等于 4MB的大对象;

Compacting

Compacting:整理内存。

因为 ZGC回收器和 CMS、G1等垃圾回收器一样,使用了”标记-复制算法”,该算法会产生内存碎片,因此需要进行内存整理操作,清除内存碎片。

NUMA

NUMA,Non-Uniform Memory Access(非一致内存访问)。

最初的计算机是单核处理器,一个 CPU访问一块内存,但是随着网络的快速发展,单核远不能满足实际需求,因此采用多核处理器技术,多 CPU需要访问同一个内存,因为任一 CPU对同一内存的访问速度是一致的,所以也称作一致内存访问(Uniform Memory Access, UMA),再随着网络的发展,单内存无法满足需求,因此就诞生了多内存,把 CPU和内存集成到同一个单元,这样 CPU就会访问离它最近的内存,提升读写效率,这种方式就是非一致内存访问。

NUMA和UMA比较如下图:

img.png

NUMA 架构在中大型系统上非常流行,是一种高性能的解决方案,ZGC就充分利用 NUMA架构的特征。

Colored pointers

Colored pointers:染色指针,一种将数据存放在指针里的技术,JVM是通过染色指针来标识某个对象是否需要被GC。 像 CMS,G1这些垃圾收集器的 GC信息都保存在对象头中,而 ZGC的 GC信息保存在指针中,每个对象有一个 64位指针,参考 JDK zGlobals_x86.cpp 源码,ZGC 地址空间和指针结构有如下 3种布局:

布局1

  • [0 ~ 41] 共 42-bits,对应 4TB的 Java堆内存;
  • [42-45] 共 4-bits,对应 Metadata Bits,存放 Marked0,Marked1,Remapped,Finalizable 元数据;
  • [46-63] 共 18-bits,全部存放0,预留未使用; img.png

抽象成结构图如下:

img.png

布局2

  • [0 ~ 42] 共 43-bits,对应 8TB的 Java堆内存;
  • [43-46] 共 4-bits,对应 Metadata Bits,存放 Marked0,Marked1,Remapped,Finalizable 元数据;
  • [47-63] 共 17-bits,全部存放0,预留未使用; img.png

抽象成结构图如下:

img.png

布局3

  • [0 ~ 43] 共 44-bits,对应 16TB的 Java堆内存;
  • [44-47] 共 4-bits,对应 Metadata Bits,存放 Marked0,Marked1,Remapped,Finalizable 元数据;
  • [48-63] 共 16-bits,全部存放0,预留未使用; img.png

抽象成结构图如下:

img.png

通过上面的 3种布局,可以发现一个共性:分配 4-bits来分别存放 Marked0,Marked1,Remapped,Finalizable 染色标记位,4种染色标记位说明如下:

  • Marked0,1-bit,用于标记可到达的对象(活跃对象);
  • Marked1,1-bit,用于标记可到达的对象(活跃对象);
  • Remapped,1-bit,指向最新的并指向该对象的当前位置;
  • Finalizable,1-bit,标识这个对象只能通过finalizer才能访问;

多重视图

上述 Marked0,Marked1,Remapped染色标记位其实代表一种地址视图,当应用程序创建对象时,首先在堆空间申请一个虚拟地址,ZGC同时会为该对象在 Marked0、Marked1和 Remapped地址空间分别申请一个虚拟地址,三个虚拟地址指向同一个物理地址,并且在同一时间,三个虚拟地址有且只有一个空间有效,整个视图映射关系如下图:

img.png

ZGC之所以设置三个虚拟地址空间,目的是使用”虚拟空间换时间”的思想,从而降低GC停顿时间。三个空间的切换是由垃圾回收的不同阶段触发的,通过限定三个空间在同一时间点有且仅有一个空间有效高效的完成GC过程的并发操作。

Load barriers

Load barriers:读屏障,是指 JIT( just-in-time compilation 即时编译器,JVM)向应用代码注入一小段代码,当从堆中读取对象引用时,就会执行这段代码,官方说明如下:

img.png

读屏障示例:

1
2
3
4
5
6
7
8
9
10
11
String n = person.name;    // 从堆中读取引用,需要加入屏障

<Load barrier start>
  if (n & bad_bit_mask) {
      slow_path(register_for(n), address_of(person.name));
  }
<Load barrier end>

String p = n ;         // 无需屏障,不是从堆中读取引用
n.isEmpty() ;          // 无需屏障,不是从堆中读取引用
int age = person.age;    // 无需屏障,不是对象引用

在读屏障示例中,JVM 注入了如下的一段读屏障代码:

1
2
3
  if (n & bad_bit_mask) {
     slow_path(register_for(n), address_of(person.name));
  }

对应的字节码如下:

1
2
3
4
5
mov 0x10(%rax), %rbx     // String n = person.name;
test %rbx, 0x20(%r15)    // Bad color?
jnz slow_path            // Yes -> Enter slow path and
// mark/relocate/remap, adjust
// 0x10(%rax) and %rbx

假如 person 对象发生移动,因此 n 和 person.name 的地址都会发生变化,当使用 n 前,需要判断 n 的染色指针是否为 good,如果为 bad color,可以得知 n 的引用地址被修改过,因此需要修正 n 和 person.name的地址,整个过程如下图:

img.png

上图过程也称为”自愈”,即当对象地址发生转移时,通过读屏障操作,不仅赋值的引用更改为最新值,自身引用也被修正了,整个过程看起来像自愈。

这里对”转移”做个解释:ZGC是按照 Page内存页进行垃圾回收的,也就是说当对象所在的页面需要回收时,页面里还存活的对象需要被转移到其他页。

Concurrent

Concurrent:并发,指 GC线程和应用线程是并发执行。

和 MCS、G1等垃圾回收器一样,ZGC也采用了标记-复制算法,不过,ZGC对标记-复制算法做了很大的改进,ZGC垃圾回收周期和视图切换可以抽象成下图:

img.png

  • 初始化阶段:ZGC初始化之后,整个堆内存空间的地址视图被设置为 Remapped;
  • 标记阶段:当进入标记阶段时,视图转变为 Marked0 或者 Marked1;
  • 转移阶段:从标记阶段结束进入转移阶段时,视图再次被设置为 Remapped;

下图以 obj1,obj2,obj3 三个对象为案例对垃圾回收和视图切换进行了演示:

img.png

ZGC 全过程

ZGC垃圾回收全过程包含:初始标记、并发标记、再标记、并发转移准备、初始转移、并发转移 6个阶段,如下图:

img.png

在 ZGC 全过程中,会出现 3个 STW(Stop The World):初始标记,再标记,初始转移。尽管这 3个阶段会 STW,但是 ZGC对 STW的暂停时间是有严格要求,一般是 1ms 甚至更低,下面将分别介绍各个阶段:

  • 初始标记:这个阶段会 STW,仅标记 GC Root直接可达的对象,压到标记栈中;
  • 并发标记,重新定位:这个阶段,GC线程和应用线程是并发执行的,根据初始标记的对象开始并发遍历对象图,还会统计每个 region 的存活对象的数量,具体流程如下图:

img.png

  • 再标记:这个阶段会 STW,因为并发阶段应用线程还在运行,所以可能会修改对象的引用,导致漏标记的情况,因此,再标记阶段会标记这些漏标的对象,另外,这个阶段还会对系统字典、JVMTI、JFR、字符串表等非强根进行并行标记;
  • 并发转移准备:为初始转移做一些前置工作;
  • 初始转移:从GC Root Set集合出发,如果对象在转移的分区集合中,则在新的分区分配对象空间;
  • 并发转移:这个阶段,GC线程和应用线程是并发执行的,GC线程和应用线程操作对象流程如下图:

img.png

常见问题

为什么需要 Marked0 和 Marked1 两个标识?

简单地说是为了区别上一次标记和当前标记,因为每个 GC周期开始时,会交换使用的 Marked标记位,使上次 GC周期中修正的已标记状态失效,所有引用都变成未标记。 比如:GC周期1 使用Marked0,则 GC周期结束后,所有引用 Marked0标记都会成为 01(二进制的01 = 0);GC周期2使用 Marked1,类同于周期1,所有的 Marked标记都会成为 10(二进制的10 = 1)。

为什么会有 3种内存布局?

这主要还是和 ZGC的目标(支持 8MB-16TB 的堆)相匹配,因为 ZGC需要支持最大 16TB Java堆内存的垃圾回收,所以就需要用不同 bit位数来表示,因此就出现了3种布局。

使用多重视图和染色指针的优点

像 CMS,G1等垃圾回收器,GC信息是存放在对象头中,因此每次修改对象头信息时都需要先访问内存,然后操作,而 ZGC是把 GC信息存放在指针的有色标记位上,修改GC信息时,无需任何对象访问,只需要设置地址中对应的标志位即可,因此可以加快标记和转移的速度,这也是 ZGC在标记和转移阶段速度更快的原因。

Supported Platforms

ZGC 支持哪些平台?

下面是官方文档列举的所有支持平台:目前,ZGC目前支持了大多数的操作系统,并且是 64位系统。

img.png img.png

重要配置参数

启用 ZGC

1
2
3
-XX:+UseZGC -Xmx<size> -Xlog:gc
# 或者
-XX:+UseZGC -Xmx<size> -Xlog:gc*

ZGC的 JVM选项

img.png

  • -Xms -Xmx:堆的最大内存和最小内存;
  • -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize:设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m;
  • -XX:+UnlockExperimentalVMOptions -XX:+UseZGC:启用ZGC的配置;
  • -XX:ConcGCThreads:并发回收垃圾的线程。默认是总核数的12.5%,8核CPU默认是1。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响;
  • -XX:ParallelGCThreads:STW阶段使用线程数,默认是总核数的60%。 -XX:ZCollectionInterval:ZGC发生的最小时间间隔,单位秒;
  • -XX:ZAllocationSpikeTolerance:ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC;
  • -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否启用主动回收,默认开启,这里的配置表示关闭;
  • -Xlog:设置GC日志中的内容、格式、位置以及每个日志的大小;

ZGC的发展

ZGC 最初是作为 JDK 11 中的一项实验性功能引入的,并在 JDK 15 中被宣布为 Production Ready

从 不支持类卸载 到 支持类卸载

从仅支持个别系统到支持多个平台

从不支持指针压缩,到支持压缩类指针

JDK 16 支持并发线程堆栈扫描

……

ZGC 随着JDK发展的更改日志如下:

img.png

通过 ZGC的发展可以看出:GC垃圾回收器已经越来越智能化,GC会自适应各种情况自动优化。

总结

  • ZGC是一个可扩展的低延迟垃圾收集器,能够处理TB级别堆内存的垃圾回收;
  • ZGC垃圾回收过程几乎全是并发,实际 STW 停顿时间极短;
  • ZGC相对于CMS,G1这些垃圾回收器,最大的技术区别点是染色指针、读屏障、内存多重映射;
  • ZGC 和 Shenandoah、G1一样,采用基于 Region的堆内存分布;
  • 对吞吐量优先的场景,ZGC可能并不适合;
  • ZGC将对象存活信息存储在染色指针中,而传统垃圾回收器将对象存活信息放在对象头中;
  • 尽管目前大多数互联网公司,主流使用 jdk 8、jdk 11,垃圾回收器使用的是 ParNew + CMS 组合或 G1,但是,作为技术人员还是建议保持对新技术的热情;

参考

Per Liden ZGC 2018年讲解

Per Liden ZGC 2020年讲解

Per Liden ZGC 2022年讲解

Per Liden 个人网站

ZGC 官网

ZGC PDF

鸣谢

如果你觉得本文章对你有帮助,感谢转发给更多的好友,关注我:猿java,为你呈现更多的硬核文章。

drawing

Java 小知识:JDK版本这样多,该如何选择?

Redis 分布式锁