Java最前沿技术——ZGC
ZGC介绍
该垃圾收集器由JDK 11推出,并旨在实现极低延迟回收机制的研究与优化。
- 最大停顿时间为10毫秒;
- 停顿时间与堆的大小或活跃对象的数量无关;
- 支持从8MB到4TB不等的内存块,并承诺未来将扩展至16TB。
当初,提出这个目标的时候,有很多人都觉得设计者在吹牛逼。
但今天看来,这些“吹下的牛逼”都在一个个被实现。
借助最新的JDK 15版本,“运行时停顿时间不超过10ms”以及具备“最大堆内存可达16TB”的两个目标已达成;官方已明确说明JDK 15中的ZGC已从实验性质发展为成熟可靠的技术方案,并建议将其投入实际应用中。
ZGC已经熟了,面试题还会远吗?
本文将基于ZGC的设计理念展开阐述,在低延迟应用场景中探讨其显著的效果。
核心技术
多重映射
为了能更好的理解ZGC的内存管理,我们先看一下这个例子:
你在父母眼中是一个受爱的人,在伴侣眼中是一个伴侣。在全球人的注视下是最美的存在。您有一个真实身份,请不要对外公开。将这一关系以映射图的形式呈现:

- 从你的父亲看来,在他的眼中/看法下/视角里/角度上/层面来看,在他/她的视角下(你可以视为)相当于儿子。
- 从你的女朋友的角度出发,在她的角度来看,在她的视角下(你可以被称为)你会被视为男朋友。
- 从全球范围来看,在这个大背景下,在这个大框架内(你会被视为)会被认为是最帅的人。
假设您的名字在全球独一无二,
您可以使用以下几种方式确认自己的身份:
- 您个人的独特标识
- 您父亲的儿子的身份证明
- 您女朋友的男朋友提供的信息
通过这种方式(指最帅的人),最终都能精准定位到您本人。
现在我们再来看看ZGC的内存管理。
ZGC以高效和灵活的方式进行内存管理,并实现了并建立了物理内存与虚拟内存之间的对应关系。这与其操作系统中对虚拟地址与物理地址的设计思路具有相似的设计理念。
当应用程序创建对象时,在堆内存中分配了一个虚拟地址块,并且ZGC会在Marked0、Marked1和Remapped三个视图空间各自分配对应的虚拟地址块。这些虚拟地址最终都会映射到同一个物理内存位置。

图中的Marked0、Marked1和Remapped三个视图是什么意思呢?
参考上文中的示例
而三个视图中的地址均为虚拟地址相当于你爸爸眼中所认为的儿子;以及你女朋友眼中所认为的男朋友
最后综上所述,在此方案中这些虚地址都可以映射到同一个物理地址这一位置上,并且该物理地址对应上述实例中的"个人身份标识符"
用一段简单的Java代码表示这种关系:

在ZGC中这三个空间在同一时间点有且仅有一个空间有效。
这种设计理念的核心在于通过虚拟空间与时间资源的巧妙平衡来优化效率。这一设计理念展现了ZGC系统在内存管理和性能方面的卓越之处。系统采用三个切换区域的空间转换由垃圾回收的不同阶段所驱动的方式进行管理,在保证内存使用效率的同时实现了多线程环境下的高效运行机制。通过限制三个切换区域在同一时间段内仅有单一区域处于有效状态来实现对内存碎片化问题的有效规避,并在此基础上结合并行计算的优势实现了整体运行效率的最大化提升。具体实现细节将在后续章节中详细介绍
染色指针
在讲ZGC并发处理算法之前,还需要补充一个知识点——染色指针。
众所周知,在早期的垃圾回收机制中,所有GC信息(包括标记信息以及不同分代的年龄数据)都被存储在对象头中的一个字段Mark Word中。例如:
如果某个人被贴上"污损人物"的标签,在他的头顶放置一个"污损人物"的标记;如果这个人不再属于那个类别,则移除这个标记。
而ZGC是这样做的:
如果一个人被归类为'垃圾'。就给这个人的身份证信息里贴上'垃圾'标签。无论他今后在何处刷身份证都会被标记为'病毒体'(Virus Person)。或许有一天他会意识到自己不再是那个'病毒体'的人并选择清除自己的标记。
在这例子中,“这个人”就是一个对象,而“身份证”就是指向这个对象的指针。
ZGC采用了信息的彩色存储方式,这一技术以一种优雅的名字被描述为染色指针(Colored Pointer)。

在64位的机器中,对象指针是64位的。
- ZGC在64bit地址空间中采用了从第0到第43位来存储对象地址(其中2^44等于16TB),因此ZGC能够支持的最大堆大小为16TB。
- 其中第44到第47位被用作颜色标志字段,在此字段下Marked0和Marked1分别代表两个视图标记位(即Remapped),而Finalizable字段则表示该对象仅能通过finalizer方法进行访问。
- 需要注意的是,在第59到第63位的位置上已经固定设置为零且未被利用。
读屏障
读屏障是JVM向应用代码注入少量代码的技术。当应用线程从堆中获取对象引用时会触发这段代码。切勿将这里的术语与Java内存模型中的术语混淆两者本质上不同的是ZGC中的read barriers是一个面向切面编程的技术它在字节码层面或编译时进行处理
读屏障实例:

Object o = obj.FieldA // 从堆中读取对象引用,需要加入读屏障
<load barrier needed here>
Object p = o // 无需加入读屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入读屏障,因为不是从堆中读取引用
int i = obj.FieldB // 无需加入读屏障,因为不是对象引用
ZGC中读屏障的代码作用:
在并发执行机制下运行的应用程序中存在以下情况:当一个应用程序进程(AP)访问到A对象内部引用的对象B时(即该进程试图通过A间接访问到B),由于系统设计的原因(如引入了回收站指针机制),该对象B正接受回收站进程(GC)的操作或可能被回收站进程移动中。考虑到读屏障的影响后(即程序必须等待回收站相关操作完成才能继续),应用程序进程将触发对目标对象B的状态检查并采取相应措施以确保数据一致性与可用性。具体的探测与处理流程如下:

这样会影响程序的性能吗?
可能会出现某种程度上的性能损失。经测试表明,在这种情况下可能出现约四分之一左右的性能损失。然而这被认为是ZGC并行转移的核心基础,在减少同步窗口方面具有重要意义。
ZGC并发处理算法
ZGC并发处理算法基于切换机制下的全局空间视图与对象地址视图切换,并通过配合SATB算法实现高效的并行处理能力。
前面所做的所有铺垫工作都是为了阐述ZGC并行处理机制这一核心内容,在一些博客文章中提到过染色指针和读屏障作为ZGC的关键组成要素之一,但对其具体作用机制却鲜有深入探讨。我认为并行处理机制才是ZGC最为关键的核心要素,在这一基础之上相关技术手段才得以实现。而这些辅助工具(染色指针和读屏障)仅是为了辅助实现这一核心机制服务的辅助性技术手段。
ZGC的并发处理算法三个阶段的全局视图切换如下:
- 初始化阶段:在ZGC初始化完成后, 内存空间的整体地址视图被配置为其Remapped状态.
- 标记阶段: 该地址视图在进入标记操作后会转变为Marked0(即M0), 或者转变为Marked1(即M1).
- 转移阶置: 在完成当前操作并转入下一步骤前, 内存地址视图会被重新配置回其Remapped状态.

标记阶段
标记阶段全局视图切换为M0视图。由于应用程序与标记线程处于并发执行状态,则可能导致对象的访问可能源自这两个不同的线程之中。

在标记阶段结束之后,对象的地址视图要么是M0,要么是Remapped。
- 当对象的地址视图显示为M0时,则表明该对象当前处于活跃状态;
- 当对象的地址视图为Remapped状态时,则表明该对象当前处于非活跃状态;换言之,在这种情况下该对象所占用的内存空间已可被回收。
在标记阶段完成后,ZGC将存储所有活跃对象的地址信息于对象活跃信息表中,并确保其对应的地址视图均为M0。

转移阶段
在切换阶段转至Remapped视图进行操作。由于程序与迁移线程均处于并发执行状态,在这种情况下,在程序层面与迁移层面的访问可能会发生重叠。

至此,ZGC的一个垃圾回收周期中,并发标记和并发转移就结束了。
为何要设计M0和M1
我们在标记阶段涉及了两个地址视图M0和M1,在该算法过程中仅使用了一个地址视图。那么为什么要设置为两个?具体来说,则是为了区分上一次标记与当前标记。
ZGC是基于页面的机制执行部分内存碎片回收操作。具体而言,在对象所在的页面计划执行垃圾回收任务时,则会将该页面中的对象进行迁移;而如果当前所在的页面无需执行迁移操作,则其中的对象也就不会被转移。

如下所示,在执行至第二次GC周期启动时
M1:本次垃圾回收中识别的活跃对象。
M0:上一轮垃圾回收时已标记过的活跃对象集合,在处理过程中这些对象没有被转移出去,并在当前轮次的垃圾回收中被系统判定为暂时不再活跃的状态。
Remapped: 上次垃圾回收中的转移阶段涉及的目标对象或者是程序线程操作的目标对象,在当前次垃圾回收过程中被识别为非活跃对象。
现在,我们可以回答“使用地址视图和染色指针有什么好处”这个问题了
凭借地址视图与染色指针技术的应用能够显著提升标记与转移的速度。旧垃圾回收机制依靠修改对象头中的标记位以实现GC信息的标记这一过程涉及内存存取操作 而现代垃圾回收算法如ZGC则采用了地址视图与染色指针技术 在无需任何对象访问的情况下仅需配置相关标志位即可完成任务
一旦某个对象被判定为无用的时候
ZGC步骤
基于标记-复制机制的ZGC实现中,在标记阶段、转移阶段以及重新定位阶段之间实现了并行处理

ZGC只有三个STW阶段:初始标记 ,再标记 ,初始转移 。
其中,在处理过程中仅需各自扫描所有的GC Roots;其处理时间与其数量呈正比关系,并且通常情况下耗时极短。
在重叠窗口期间(SW),再标记阶段的时间通常仅持续至多1毫秒;如果重叠窗口期超过1毫秒,则会触发并行再标记阶段。具体而言,在ZGC体系中几乎所有暂停操作都基于GC Roots集合的大小进行管理;这些暂停所耗的时间不会因堆内存的整体规模或者活跃对象数量的变化而发生改变。与ZGC相比,在G1回收机制中切换到重叠窗口期的过程完全依赖于STW机制,并且其停顿时长会随着存活对象数量的增长而逐步延长。
ZGC的发展
ZGC诞生于JDK11,经过不断的完善,JDK15中的ZGC已经不再是实验性质的了。
经历了从仅限于Linux/x64系统向多种平台扩展的过程;经过了从不处理指针压缩到能够处理包含指针压缩功能的各种场景

在JDK16发布时,ZGC系统将推出支持并发线程栈扫描的新功能. 基于SPECjbb2015测试数据的结果, 实现该功能后,ZGC的单线程吞吐量能够减少一个数量级. 这将确保ZGC系统在极端负载下的稳定运行, 并使停顿时间降至毫秒级别.

目前 ZGC 已经发展成为一款性能卓越的垃圾收集器。它沿用 Pauseless GC 的理念,并逐步向 C4 代垃圾收集器靠拢,在这一过程中引入了分代收集的概念。
Oracle做出了重要贡献,在商业应用层面取得了显著进展的可能性。伴随JDK版本的不断更新迭代,在某个特定时间点上, 我坚信垃圾回收机制能够灵活应对不同运行环境的变化, 从而确保系统性能最佳状态.
可以说ZGC是Java领域的前沿技术,在当前G1尚未得到广泛应用的情况下讨论其应用前景似乎有些 premature。或许我们应该关注的重点可能在于其背后的设计理念而非具体的技术实现。
希望你能有所收获!
