你好,我是猿java。
今天我们分享的内容是:新一代 Java垃圾回收神器:ZGC。
ZGC 定义
ZGC(The Z Garbage Collector),是一种可扩展的低延迟垃圾收集器,主要是用来处理超大内存(TB级别)的垃圾回收。
ZGC 最初是 JDK 11 以一项实验性功能引入的,经过几个版本的迭代,最终在 JDK 15中被宣布为 Production Ready。
ZGC的中的"Z"代表什么含义?官方解释如下:
大概意思为:Z它不代表任何东西,ZGC只是一个名字。它最初受到 ZFS(文件系统)的启发或致敬,ZFS(文件系统)在首次问世时在许多方面都是革命性的。最初,ZFS 是“Zettabyte File System”的首字母缩写词,但这个意思被废弃了,后来据说它不代表任何东西。这只是一个名字。有关详细信息,请参阅Jeff Bonwick 的博客。
下图为 Oracle官方对 ZGC停顿时间的描述:
ZGC的目标
2018年,Oracle官方描述的 ZGC目标为:
2022年11月,Oracle官方描述的 ZGC的目标为:
通过官方对 ZGC目标的描述也可以看出,在几年的时间内,ZGC垃圾回收器的最大停顿时间已从 10ms 降低到 亚毫秒 级别,性能有了质的飞越。
核心技术点
2018年,官方描述的 ZGC的核心技术点为:
- Concurrent:并发
- Tracing: 可追踪
- Compacting:整理内存
- Single generation: 单代,也就是不进行分代
- Region-based:基于Region
- NUMA-aware:支持NUMA,Non-Uniform Memory Access(非一致内存访问)
- Load barriers:读屏障
- Colored pointers:染色指针,一种将信息存储在指针中的技术
2022年,官方描述的 ZGC的核心技术点为:
- Concurrent:并发
- Region-based:基于Region
- Compacting:整理内存
- NUMA-aware:支持NUMA,Non-Uniform Memory Access(非一致内存访问)
- Using colored pointers:使用染色指针,一种将信息存储在指针中的技术
- Using load barriers:使用读屏障
比较 2018年 和 2022年官方对 ZGC核心技术的描述可以发现,官方把 Single generation 这一点去掉了,熟悉 G1的小伙伴应该知道,G1是 Region-based类型,每个 Region在同一时刻只能属于一种分代,但是,一个Region可以在多个分代之间动态切换,因此,ZGC 从 最初的不分代发展成和 G1一样,也有分代。
ZGC 原理
Region-based
Region-based:基于区域。
ZGC 和 G1等垃圾回收器一样,也会将堆划分成很多的小分区,整个堆内存分区如下图:
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比较如下图:
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,预留未使用;
抽象成结构图如下:
布局2
- [0 ~ 42] 共 43-bits,对应 8TB的 Java堆内存;
- [43-46] 共 4-bits,对应 Metadata Bits,存放 Marked0,Marked1,Remapped,Finalizable 元数据;
- [47-63] 共 17-bits,全部存放0,预留未使用;
抽象成结构图如下:
布局3
- [0 ~ 43] 共 44-bits,对应 16TB的 Java堆内存;
- [44-47] 共 4-bits,对应 Metadata Bits,存放 Marked0,Marked1,Remapped,Finalizable 元数据;
- [48-63] 共 16-bits,全部存放0,预留未使用;
抽象成结构图如下:
通过上面的 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地址空间分别申请一个虚拟地址,三个虚拟地址指向同一个物理地址,并且在同一时间,三个虚拟地址有且只有一个空间有效,整个视图映射关系如下图:
ZGC之所以设置三个虚拟地址空间,目的是使用"虚拟空间换时间"的思想,从而降低GC停顿时间。三个空间的切换是由垃圾回收的不同阶段触发的,通过限定三个空间在同一时间点有且仅有一个空间有效高效的完成GC过程的并发操作。
Load barriers
Load barriers:读屏障,是指 JIT( just-in-time compilation 即时编译器,JVM)向应用代码注入一小段代码,当从堆中读取对象引用时,就会执行这段代码,官方说明如下:
读屏障示例:
String n = person.name; // 从堆中读取引用,需要加入屏障
if (n & bad_bit_mask) {
slow_path(register_for(n), address_of(person.name));
}
String p = n ; // 无需屏障,不是从堆中读取引用
n.isEmpty() ; // 无需屏障,不是从堆中读取引用
int age = person.age; // 无需屏障,不是对象引用
在读屏障示例中,JVM 注入了如下的一段读屏障代码:
if (n & bad_bit_mask) {
slow_path(register_for(n), address_of(person.name));
}
对应的字节码如下:
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的地址,整个过程如下图:
上图过程也称为"自愈",即当对象地址发生转移时,通过读屏障操作,不仅赋值的引用更改为最新值,自身引用也被修正了,整个过程看起来像自愈。
这里对"转移"做个解释:ZGC是按照 Page内存页进行垃圾回收的,也就是说当对象所在的页面需要回收时,页面里还存活的对象需要被转移到其他页。
Concurrent
Concurrent:并发,指 GC线程和应用线程是并发执行。
和 MCS、G1等垃圾回收器一样,ZGC也采用了标记-复制算法,不过,ZGC对标记-复制算法做了很大的改进,ZGC垃圾回收周期和视图切换可以抽象成下图:
- 初始化阶段:ZGC初始化之后,整个堆内存空间的地址视图被设置为 Remapped;
- 标记阶段:当进入标记阶段时,视图转变为 Marked0 或者 Marked1;
- 转移阶段:从标记阶段结束进入转移阶段时,视图再次被设置为 Remapped;
下图以 obj1,obj2,obj3 三个对象为案例对垃圾回收和视图切换进行了演示:
ZGC 全过程
ZGC垃圾回收全过程包含:初始标记、并发标记、再标记、并发转移准备、初始转移、并发转移 6个阶段,如下图:
在 ZGC 全过程中,会出现 3个 STW(Stop The World):初始标记,再标记,初始转移。尽管这 3个阶段会 STW,但是 ZGC对 STW的暂停时间是有严格要求,一般是 1ms 甚至更低,下面将分别介绍各个阶段:
- 初始标记:这个阶段会 STW,仅标记 GC Root直接可达的对象,压到标记栈中;
- 并发标记,重新定位:这个阶段,GC线程和应用线程是并发执行的,根据初始标记的对象开始并发遍历对象图,还会统计每个 region 的存活对象的数量,具体流程如下图:
- 再标记:这个阶段会 STW,因为并发阶段应用线程还在运行,所以可能会修改对象的引用,导致漏标记的情况,因此,再标记阶段会标记这些漏标的对象,另外,这个阶段还会对系统字典、JVMTI、JFR、字符串表等非强根进行并行标记;
- 并发转移准备:为初始转移做一些前置工作;
- 初始转移:从GC Root Set集合出发,如果对象在转移的分区集合中,则在新的分区分配对象空间;
- 并发转移:这个阶段,GC线程和应用线程是并发执行的,GC线程和应用线程操作对象流程如下图:
常见问题
为什么需要 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位系统。
重要配置参数
启用 ZGC
-XX:+UseZGC -Xmx -Xlog:gc
# 或者
-XX:+UseZGC -Xmx -Xlog:gc*
ZGC的 JVM选项
-
-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发展的更改日志如下:
通过 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