如何将AI能力与大数据技术结合,助力数据分析治理等工作的效率大幅提升,优化大数据引擎的性能及成本? 了解详情
写点什么

Oracle 即将发布的全新 Java 垃圾收集器 ZGC

  • 2018-09-11
  • 本文字数:0 字

    阅读完需:约 1 分钟

Java 11 的特性集合已经确定,其中包含了一些非常棒的特性。新版本提供了一个全新的垃圾回收器 ZGC,它由甲骨文开发,承诺在 TB 级别的堆上实现非常低的停顿时间。在本文中,我们将介绍甲骨文开发 ZGC 的动机、ZGC 的技术概览以及 ZGC 带来的一些非常令人兴奋的可能性。

那么为什么要开发 ZGC?毕竟 Java 10 中已经带有 4 款久经考验的垃圾回收器。Hotspot 最新的垃圾回收器 G1 是在 2006 年推出的。当时最大的 AWS 实例是 m1.small,配备 1 个 vCPU 和 1.7GB 内存,而到了今天,AWS 提供了 x1e.32xlarge 实例,配备了 128 个 vCPU 和令人难以置信的 3,904GB 内存。ZGC 所针对的是这些在未来普遍存在的大容量内存:TB 级别的堆容量,具有很低的停顿时间(小于 10 毫秒),对整体应用性能的影响也很小(对吞吐量的影响低于 15%)。ZGC 所采用的机制也可以在未来进行扩展,以支持一些令人兴奋的特性,如多层堆(用于热对象的 DRAM 和用于低频访问对象的 NVMe 闪存)或压缩堆。

GC 术语

要了解 ZGC 在现有垃圾回收器中所处的位置,以及它是如何达到这个位置的,我们先需要先了解一些术语。最基本的 GC 包括识别出不再使用的内存,并将其变为可用的。现代垃圾回收器通常分几个阶段来完成回收过程,如下所示:

  • 并行(Parallel)——运行中的 JVM 包含应用程序线程和 GC 线程。在并行阶段,会运行多个 GC 线程,也就是说任务被拆分给它们去完成。至于 GC 线程是否可以与正在运行的应用程序线程重叠,这个在规范中并没有特别说明。
  • 串行(Serial)——串行阶段只有单个 GC 线程在运行。与上面的并行阶段一样,规范中也没有说明 GC 线程是否可以与当前运行的应用程序线程重叠。
  • Stop The World(STW)——在这个阶段,应用程序线程被暂停,让 GC 线程执行它们的任务。当你遇到 GC 停顿时,说明虚拟机进入了 STW 阶段。
  • 并发(Concurrent)——在并发阶段,GC 线程可以在运行应用程序线程的同时执行自己的任务。并发阶段非常复杂,因为应用程序线程有可能在 GC 完成之前将其中断。
  • 增量(Incremental)——在增量阶段,它可以运行一段时间,并基于某些条件提前终止,例如时间预算或执行更高优先级的 GC 阶段。

权衡取舍

需要指出的是,所有这些属性都存在权衡。例如,并行阶段将利用多个 GC 线程来执行任务,但这样做会导致协调线程的开销。同样,并发阶段不会暂停应用程序线程,但可能涉及更多的开销和复杂性。

ZGC

在了解了 GC 不同阶段的属性后,现在让我们来探讨 ZGC 的工作原理。ZGC 使用了两项新技术:彩色指针和加载屏障。

指针着色

指针着色是将信息存储在指针(或引用)中的一种技术。这是有可能的,因为在 64 位平台上(ZGC 仅支持 64 位),指针可以处理比系统实际拥有的内存更大的内存,因此可以使用多余的位来存储状态。ZGC 将堆限制为 4TB,需要 42 位,剩下的 22 位当中目前已经使用了 4 位:finalizable、remap、mark0 和 mark1。

不过,指针着色也存在一个问题,当你想要取消引用指针时,需要做额外的工作,因为你需要屏蔽掉信息位。SPARC 平台已经为指针屏蔽提供了内置硬件支持,所以这不是什么问题。但 x86 平台还没有提供类似的支持,所以 ZGC 团队针对 x86 平台使用了多次映射技术。

多次映射

要了解多映射的工作原理,我们需要先简要地解释一下虚拟内存和物理内存之间的区别。物理内存是系统可用的实际内存,也就是 DRAM 芯片的容量。虚拟内存是抽象的,对于应用程序来说,它们有自己的物理内存试图(通常是隔离的)。操作系统负责维护虚拟内存和物理内存之间的映射,通过使用页表和处理器的内存管理单元(MMU)以及转换后备缓冲区(TLB,用于转换应用程序的请求地址)来实现。

多次映射技术将不同范围的虚拟内存映射到同一物理内存上。在 remap、mark0 和 mark1 当中,同一时间点只能有一个为 1,因此可以使用三个映射。ZGC 源代码中提供了一个很直观的图表( http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hotspot/os_cpu/linux_x86/zGlobals_linux_x86.hpp#l39 )。

加载屏障

加载屏障是一小段代码,当应用程序线程从堆加载引用时就会运行这段代码(即访问对象的非原始类型字段):

复制代码
void printName( Person person ) {
String name = person.name; // 将会触发加载屏障,因为从堆中加载了一个引用
System.out.println(name); // 没有直接使用加载屏障
}

第一行代码是给变量 name 赋值,这需要跟踪堆上的 person 引用,然后再加载 name 引用。这个时候会触发加载屏障。第二行代码在屏幕上打印 name,不会直接触发加载屏障,因为不需要加载堆引用——name 是局部变量,因此不需要从堆加载引用。不过,System 和 out,或者 println 内部可能会触发其他加载屏障。

这与其他垃圾回收器(例如 G1)使用的写入屏障形成对比。加载屏障的任务是检查引用的状态,并在将引用(或者不同的引用)返回给应用程序之前执行一些任务。在 ZGC 中,它会对加载的引用进行测试,查看是否设置了某些位,具体取决于当前处于哪个阶段。如果引用通过测试,就不执行任何其他操作,如果没有通过,就会在将引用返回给应用程序之前执行一些特定于当前阶段的操作。

标记

在了解了这两项新技术后,现在让我们来看看 ZGC 的 GC 周期。GC 周期的第一部分是标记,就是以某种方式查找并标记应用程序可以访问到的所有堆对象,换句话说,就是查找非垃圾对象。

ZGC 的标记分为三个阶段。第一阶段是 STW,在这一阶段,GC root 被标记为存活。GC root 类似于局部变量,应用程序使用它们来访问堆上的其他对象。从 GC root 开始遍历对象图,如果某些对象无法被访问到,那么应用程序也就无法访问到这些对象,它们就被认为是垃圾。可以从 GC root 访问到的对象集被称为存活集。GC root 标记步骤所需要的时间非常短,因为 GC root 的总量通常相对较少。

标记阶段完成后,应用程序恢复运行,而 ZGC 将开始下一阶段,发遍历对象图,并标记所有可访问的对象。在这一阶段,加载屏障会检查所有已加载的引用,看看它们的掩码是否已经针对这一阶段进行过标记,如果尚未标记,就将其添加到待标记队列。

在完成这一步后,会出现一个短暂的 STW 阶段,它会处理一些边缘情况,然后整个标记过程就完成了。

重定位

GC 周期的下一个主要部分是重定位。重定位就是要移动存活对象,以便释放部分堆空间。为什么要移动对象而不是填补空隙?有些 GC 确实是这样做的,但这样会造成不好的后果,即堆分配将变得非常昂贵,因为在分配堆空间时,分配器需要找到放置对象的空闲空间。相反,如果可以释放大块内存,堆空间分配就会变得很简单,只需要将指针按照对象所需的内存量进行递增就可以了。

ZGC 将堆分成页,在开始进行重定位时,它会选择一组需要重新定位的存活对象的页。在选择好重定位集后,会出现一次 STW 停顿,ZGC 对重定位集中的对象进行重定位,并重新映射它们对新地址的引用。与之前的 STW 一样,停顿时间取决于 root 的数量以及重定位集与存活集的比率,这个比率通常都很小。它不会随着堆大小的变化而变化,这与其他大部分垃圾回收器一样。

移动完 root 之后,下一阶段是进行并发重定位。在这个阶段,GC 线程遍历重定位集,并重新定位页中的所有对象。如果应用程序线程尝试加载重定位集中的对象,但这些对象还未被重定位,那么应用程序线程也可以对它们进行重定位,这是通过加载屏障来实现的,如下面的流程图所示:

这样可以确保应用程序看到的所有引用都是最新的,并且应用程序不会对正在被重定位的对象做任何操作。

GC 线程最终会重定位重定位集中的所有对象,不过仍然可能存在一些指向这些对象旧地址的引用。GC 会遍历对象图,并将所有这些引用重新映射到新的地址上,但这是一个非常昂贵的步骤。所以,这一步被并入到下一个标记阶段。在标记期间,如果发现未重新映射的引用,则将其重新映射,并标记为存活。

回顾

试图单独理解复杂的垃圾回收器(如 ZGC)性能特征是很困难的,但有一点是很清楚的,我们在文中所提到的 GC 停顿都与 GC root 有关,而与存活对象集、堆大小或垃圾对象没有关系。标记阶段的最后一次停顿是一个例外,它是增量进行的,而且如果超过时间预算,GC 将恢复到并发标记,直到下一次进行尝试。

性能

那么 ZGC 的性能如何?ZGC 的 SPECjbb 2015 吞吐量数据与 Parallel GC(为吞吐量进行过优化)大致相当,平均停顿时间为 1 毫秒,最长为 4 毫秒。这与平均停顿时间超过 200 毫秒的 G1 和 Parallel 形成鲜明的对比。

未来的可能性

彩色指针和加载屏障为我们带来了一些有趣的未来可能性。

多层堆和压缩

随着闪存和非易失性内存变得越来越普及,JVM 的多层堆将成为可能,在多层堆中,很少被访问的存活对象将被保存在较慢的内存层中。

我们可以对指针元数据进行扩展,加入一些计数器位,并使用这些位信息来决定是否需要移动对象。在需要使用对象的时候,可以通过加载屏障从相应的内存层获取对象。

或者也可以不将对象重定位到较慢的内存层,而是将对象保存在主内存中,不过需要对其进行压缩。在请求对象时,通过加载屏障解对其进行解压并分配到堆中。

ZGC 的状态

在撰写本文时,ZGC 还处在实验阶段。读者可以通过 Java 11 Early Access 版本( http://jdk.java.net/11/ )来体验 ZGC,但需要指出的是,要解决一个新垃圾回收器存在的所有问题可能需要很长的一段时间。G1 从发布到脱离实验阶段花了至少三年时间。

总结

服务器拥有数百 GB 甚至是数 TB 的内存变得越来越普及,Java 有效使用内存堆的能力变得越来越重要。ZGC 是一个令人兴奋的新型垃圾回收器,致力于大幅降低大堆垃圾回收的停顿时间。它通过使用彩色指针和加载屏障来实现这一点,它们都是 Hotspot 新引入的 GC 技术,并带来了一些有趣的未来可能性。ZGC 将作为 Java 11 的实验性垃圾回收器,读者现在可以通过 Java 11 Early Access 体验 ZGC。

英文原文: https://www.opsian.com/blog/javas-new-zgc-is-very-exciting/

2018-09-11 18:265664
用户头像

发布了 731 篇内容, 共 404.6 次阅读, 收获喜欢 1963 次。

关注

评论

发布
暂无评论
发现更多内容
Oracle即将发布的全新Java垃圾收集器 ZGC_Java_Richard Warburton_InfoQ精选文章