BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 Richard Warburton 关注 0 他的粉丝 , Sadiq Jaffer 关注 0 他的粉丝 ,译者 无明 关注 2 他的粉丝 发布于 2018年9月12日. 估计阅读时间: 13 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

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/

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT