Java 11包含一个全新的垃圾收集器--ZGC,它由Oracle开发,承诺在数TB的堆上具备很是低的暂停时间。 在本文中,咱们将介绍开发新GC的动机,技术概述以及由ZGC开启的一些可能性。java
那么为何须要新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闪存),或压缩堆。程序员
为了理解ZGC如何匹配现有收集器,以及如何实现新GC,咱们须要先了解一些术语。最基本的垃圾收集涉及识别再也不使用的内存并使其可重用。现代收集器在几个阶段进行这一过程,对于这些阶段咱们每每有以下描述:微信
并行:在JVM运行时,同时存在应用程序线程和垃圾收集器线程。 并行阶段是由多个gc线程执行,即gc工做在它们之间分配。 不涉及GC线程是否须要暂停应用程序线程。并发
串行:串行阶段仅在单个gc线程上执行。与以前同样,它也没有说明GC线程是否须要暂停应用程序线程。ide
STW:STW阶段,应用程序线程被暂停,以便gc执行其工做。 当应用程序由于GC暂停时,这一般是因为Stop The World阶段。性能
并发:若是一个阶段是并发的,那么GC线程能够和应用程序线程同时进行。 并发阶段很复杂,由于它们须要在阶段完成以前处理可能使工做无效。测试
增量:若是一个阶段是增量的,那么它能够运行一段时间以后因为某些条件提早终止,例如须要执行更高优先级的gc阶段,同时仍然完成生产性工做。 增量阶段与须要彻底完成的阶段造成鲜明对比。优化
如今咱们了解了不一样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中,它经过测试加载的引用来执行此任务,以查看是否设置了某些位。 若是经过了测试,则不执行任何其余工做,若是失败,则在将引用返回给应用程序以前执行某些特定于阶段的任务。
如今咱们了解了这两种新技术是什么,让咱们来看看ZG的GC循环。
GC循环的第一部分是标记。标记包括查找和标记运行中的应用程序能够访问的全部堆对象,换句话说,查找不是垃圾的对象。
ZGC的标记分为三个阶段。 第一阶段是STW,其中GC roots被标记为活对象。 GC roots相似于局部变量,经过它能够访问堆上其余对象。 若是一个对象不能经过遍历从roots开始的对象图来访问,那么应用程序也就没法访问它,则该对象被认为是垃圾。从roots访问的对象集合称为Live集。GC roots标记步骤很是短,由于roots的总数一般比较小。
该阶段完成后,应用程序恢复执行,ZGC开始下一阶段,该阶段同时遍历对象图并标记全部可访问的对象。 在此阶段期间,读屏障针使用掩码测试全部已加载的引用,该掩码肯定它们是否已标记或还没有标记,若是还没有标记引用,则将其添加到队列以进行标记。
在遍历完成以后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘状况(咱们如今将它忽略),该阶段完成以后标记阶段就完成了。
GC循环的下一个主要部分是重定位。重定位涉及移动活动对象以释放部分堆内存。 为何要移动对象而不是填补空隙? 有些GC实际是这样作的,可是它致使了一个不幸的后果,即分配内存变得更加昂贵,由于当须要分配内存时,内存分配器须要找到能够放置对象的空闲空间。 相比之下,若是能够释放大块内存,那么分配内存就很简单,只须要将指针递增新对象所需的内存大小便可。
ZGC将堆分红许多页面,在此阶段开始时,它同时选择一组须要重定位活动对象的页面。选择重定位集后,会出现一个Stop The World暂停,其中ZGC重定位该集合中root对象,并将他们的引用映射到新位置。与以前的Stop The World步骤同样,此处涉及的暂停时间仅取决于root的数量以及重定位集的大小与对象的总活动集的比率,这一般至关小。因此不像不少收集器那样,暂停时间随堆增长而增长。
移动root后,下一阶段是并发重定位。 在此阶段,GC线程遍历重定位集并从新定位其包含的页中全部对象。 若是应用程序线程试图在GC从新定位对象以前加载它们,那么应用程序线程也能够重定位该对象,这能够经过读屏障(在从堆加载引用时触发)实现,如流程图以下所示:
这可确保应用程序看到的全部引用都已更新,而且应用程序不可能同时对重定位的对象进行操做。
GC线程最终将对重定位集中的全部对象重定位,然而可能仍有引用指向这些对象的旧位置。 GC能够遍历对象图并从新映射这些引用到新位置,可是这一步代价很高昂。 所以这一步与下一个标记阶段合并在一块儿。在下一个GC周期的标记阶段遍历对象对象图的时候,若是发现未重映射的引用,则将其从新映射,而后标记为活动状态。
试图单独理解复杂垃圾收集器(如ZGC)的性能特征是很困难的,但从前面的部分能够清楚地看出,咱们所碰到的几乎全部暂停都只依赖于GC roots集合大小,而不是实时堆大小。标记阶段中处理标记终止的最后一次暂停是惟一的例外,可是它是增量的,若是超过gc时间预算,那么GC将恢复到并发标记,直到再次尝试。
那ZGC到底表现如何?
Stefan Karlsson和Per Liden在今年早些时候的Jfokus演讲中给出了一些数字。 ZGC的SPECjbb 2015吞吐量与Parallel GC(优化吞吐量)大体至关,但平均暂停时间为1ms,最长为4ms。 与之相比G1和Parallel有不少次超过200ms的GC停顿。
然而,垃圾收集器是复杂的软件,从基准测试结果可能没法推测出真实世界的性能。咱们期待本身测试ZGC,以了解它的性能如何因工做负载而异。
本文参考:https://mp.weixin.qq.com/s/nAjPKSj6rqB_eaqWtoJsgw
欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~