一文读懂Java 11的ZGC为何如此高效

    /    2018-09-05

GC是大部分现代语言内置的特性,Java 11 新加入的ZGC号称可以达到10ms 以下的 GC 停顿,本文作者对这一新功能进行了深入解析。同时还对这一新功能带来的其他可能性做了展望。ZGC是否可以达到该性能目标,请看高可用架构志愿者翻译的文章。

Java 11的新功能已经完全冻结,其中有些功能绝对非常令人兴奋,本文着重介绍ZGC。

Java 11包含一个全新的垃圾收集器--ZGC,它由Oracle开发,承诺在数TB的堆上具有非常低的暂停时间。 在本文中,我们将介绍开发新GC的动机,技术概述以及由ZGC开启的一些可能性。

为什么需要新GC呢?

毕竟Java 10已经有四种发布多年的垃圾收集器,并且几乎都是无限可调的。 换个角度看,G1是2006年时引入Hotspot VM的。当时最大的AWS实例有1 vCPU和1.7GB内存,而今天AWS很乐意租给你一个x1e.32xlarge实例,该类型实例有128个vCPU和3,904GB内存。

ZGC的设计目标

支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。

GC术语

为了理解ZGC如何匹配现有收集器,以及如何实现新GC,我们需要先了解一些术语。最基本的垃圾收集涉及识别不再使用的内存并使其可重用。现代收集器在几个阶段进行这一过程,对于这些阶段我们往往有如下描述:

并行- 在JVM运行时,同时存在应用程序线程和垃圾收集器线程。 并行阶段是由多个gc线程执行,即gc工作在它们之间分配。 不涉及GC线程是否需要暂停应用程序线程。

串行- 串行阶段仅在单个gc线程上执行。与之前一样,它也没有说明GC线程是否需要暂停应用程序线程。

STW - STW阶段,应用程序线程被暂停,以便gc执行其工作。 当应用程序因为GC暂停时,这通常是由于Stop The World阶段。

并发 -如果一个阶段是并发的,那么GC线程可以和应用程序线程同时进行。 并发阶段很复杂,因为它们需要在阶段完成之前处理可能使工作无效(译者注:因为是并发进行的,GC线程在完成一阶段的同时,应用线程也在工作产生操作内存,所以需要额外处理)的应用程序线程。

增量 -如果一个阶段是增量的,那么它可以运行一段时间之后由于某些条件提前终止,例如需要执行更高优先级的gc阶段,同时仍然完成生产性工作。 增量阶段与需要完全完成的阶段形成鲜明对比。

权衡

值得指出的是,所有这些属性都需要权衡利弊。 例如,并行阶段将利用多个gc线程来执行工作,但这样做会导致线程协调的开销。 同样,并发阶段不会暂停应用程序线程,但可能涉及更多的开销和复杂性,才能同时处理使其工作无效的应用程序线程。

ZGC

现在我们了解了不同gc阶段的属性,让我们继续探讨ZGC的工作原理。 为了实现其目标,ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障。

着色指针

着色指针是一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,因此可以使用一些位来存储状态。 ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位: finalizable, remap, mark0和mark1。 我们稍后解释它们的用途。

着色指针的一个问题是,当您需要取消着色时,它需要额外的工作(因为需要屏蔽信息位)。 像SPARC这样的平台有内置硬件支持指针屏蔽所以不是问题,而对于x86平台来说,ZGC团队使用了简洁的多重映射技巧。

多重映射

要了解多重映射的工作原理,我们需要简要解释虚拟内存和物理内存之间的区别。 物理内存是系统可用的实际内存,通常是安装的DRAM芯片的容量。 虚拟内存是抽象的,这意味着应用程序对(通常是隔离的)物理内存有自己的视图。

操作系统负责维护虚拟内存和物理内存范围之间的映射,它通过使用页表和处理器的内存管理单元(MMU)和转换查找缓冲器(TLB)来实现这一点,后者转换应用程序请求的地址。

多重映射涉及将不同范围的虚拟内存映射到同一物理内存。 由于设计中只有一个remap,mark0和mark1在任何时间点都可以为1,因此可以使用三个映射来完成此操作。 ZGC源代码中有一个很好的图表可以说明这一点。

读屏障

读屏障是每当应用程序线程从堆加载引用时运行的代码片段(即访问对象上的非原生字段non-primitive field):

void printName( Person person ) {
    String name = person.name; // 这里触发读屏障
                              // 因为需要从heap读取引用
                              //
   System.out.println(name); // 这里没有直接触发读屏障
}

在上面的代码中,String name = person.name 访问了堆上的person引用,然后将引用加载到本地的name变量。此时触发读屏障。 Systemt.out那行不会直接触发读屏障,因为没有来自堆的引用加载(name是局部变量,因此没有从堆加载引用)。 但是System和out,或者println内部可能会触发其他读屏障。

这与其他GC使用的写屏障形成对比,例如G1。读屏障的工作是检查引用的状态,并在将引用(或者甚至是不同的引用)返回给应用程序之前执行一些工作。 在ZGC中,它通过测试加载的引用来执行此任务,以查看是否设置了某些位。 如果通过了测试,则不执行任何其他工作,如果失败,则在将引用返回给应用程序之前执行某些特定于阶段的任务。

(12)

分享至