46

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

 5 years ago
source link: http://www.infoq.com/cn/articles/oracle-release-java-gc-zgc?amp%3Butm_medium=popular_widget&%3Butm_campaign=popular_content_list&%3Butm_content=homepage
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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的总量通常相对较少。

8131-1536426497939.png

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

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

重定位

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

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

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

6532-1536426498290.png

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

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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK