关于java:Java最前沿技术ZGC

37次阅读

共计 4364 个字符,预计需要花费 11 分钟才能阅读完成。

ZGC 介绍

ZGC(The Z Garbage Collector)是 JDK 11 中推出的一款谋求极致低提早的试验性质的垃圾收集器,它已经设计指标包含:

  • 进展工夫不超过 10ms;
  • 进展工夫不会随着堆的大小,或者沉闷对象的大小而减少;
  • 反对 8MB~4TB 级别的堆(将来反对 16TB)。

当初,提出这个指标的时候,有很多人都感觉设计者在吹牛逼。

但明天看来,这些“吹下的牛逼”都在一个个被实现。

基于最新的 JDK15 来看,“进展工夫不超过 10ms”和“反对 16TB 的堆”这两个指标曾经实现,并且官网明确指出 JDK15 中的 ZGC 不再是试验性质的垃圾收集器,且倡议投入生产了。

ZGC 曾经熟了,面试题还会远吗?

本文会从 ZGC 的设计思路登程,讲清楚为何 ZGC 能在低延时场景中的利用中有着如此卓越的体现。

核心技术

多重映射

为了能更好的了解 ZGC 的内存治理,咱们先看一下这个例子:

你在你爸爸妈妈眼中是儿子,在你女朋友眼中是男朋友。在全世界人背后就是最帅的人。你还有一个名字,但名字也只是你的一个代号,并不是你自己。将这个关系画一张映射图示意:

  • 在你爸爸的眼中,你就是儿子;
  • 在你女朋友的眼中,你就说男朋友;
  • 站在全世界角度来看,你就说世界上最帅的人;

如果你的名字是全世界惟一的,通过“你的名字”、“你爸爸的儿子”、“你女朋友的男朋友”,“世界上最帅的人”最初定位到的都是你自己。

当初咱们再来看看 ZGC 的内存治理。

ZGC 为了能高效、灵便地治理内存,实现了两级内存治理:虚拟内存和物理内存,并且实现了物理内存和虚拟内存的映射关系。这和操作系统中虚拟地址和物理地址设计思路基本一致。

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,ZGC 同时会为该对象在 Marked0、Marked1 和 Remapped 三个视图空间别离申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址。

图中的 Marked0、Marked1 和 Remapped 三个视图是什么意思呢?

对照下面的例子,这三个视图别离对应的就是 ” 你爸爸眼中 ”,“你女朋友的眼中”,“全世界人眼中”。

而三个视图外面的地址,都是虚拟地址,对应的是“你爸爸眼中的儿子”,“你女朋友眼中的男朋友”……

最初,这些虚地址都能映射到同一个物理地址,这个物理地址对应下面例子中的“你自己”。

用一段简略的 Java 代码示意这种关系:

在 ZGC 中这三个空间在同一时间点有且仅有一个空间无效。

为什么这么设计呢?这就是 ZGC 的高超之处,利用虚拟空间换工夫,这三个空间的切换是由垃圾回收的不同阶段触发的,通过限定三个空间在同一时间点有且仅有一个空间无效高效的实现 GC 过程的并发操作,具体实现会在前面讲 ZGC 并发解决算法的局部再详细描述。

染色指针

在讲 ZGC 并发解决算法之前,还须要补充一个知识点——染色指针。

咱们都晓得,之前的垃圾收集器都是把 GC 信息(标记信息、GC 分代年龄..)存在对象头的 Mark Word 里。举个例子:

如果某个人是个垃圾人,就在这个人的头上盖一个“垃圾”的章;如果这个人不是垃圾了,就把这个人头上的“垃圾”印章洗掉。

而 ZGC 是这样做的:

如果某个人是垃圾人。就在这个人的身份证信息外面标注这个人是个垃圾,当前不论这个人在哪刷身份证,他人都晓得他是个垃圾人了。兴许哪一天,这个人觉悟了不再是垃圾人了,就把这个人身份证外面的“垃圾”标记去掉。

在这例子中,“这个人”就是一个对象,而“身份证”就是指向这个对象的指针。

ZGC 将信息存储在指针中,这种技术有一个高大上的名字——染色指针(Colored Pointer)。

在 64 位的机器中,对象指针是 64 位的。

  • ZGC 应用 64 位地址空间的第 0~43 位存储对象地址,2^44 = 16TB,所以 ZGC 最大反对 16TB 的堆。
  • 而第 44~47 位作为色彩标记位,Marked0、Marked1 和 Remapped 代表三个视图标记位,Finalizable 示意这个对象只能通过 finalizer 能力拜访。
  • 第 48~63 位固定为 0 没有利用。

读屏障

读屏障是 JVM 向利用代码插入一小段代码的技术。当利用线程从堆中读取对象援用时,就会执行这段代码。千万不要把这个读屏障和 Java 内存模型外面的读屏障搞混了,两者基本不是同一个货色,ZGC 中的读屏障更像是一种 AOP 技术,在字节码层面或者编译代码层面给读操作减少一个额定的解决。

读屏障实例:

Object o = obj.FieldA      // 从堆中读取对象援用,须要退出读屏障
<load barrier needed here>
  
Object p = o               // 无需退出读屏障,因为不是从堆中读取援用
o.dosomething()            // 无需退出读屏障,因为不是从堆中读取援用
int i =  obj.FieldB        // 无需退出读屏障,因为不是对象援用

ZGC 中读屏障的代码作用:

GC 线程和利用线程是并发执行的,所以存在利用线程去 A 对象外部的援用所指向的对象 B 的时候,这个对象 B 正在被 GC 线程挪动或者其余操作,加上读屏障之后,利用线程会去探测对象 B 是否被 GC 线程操作,而后期待操作实现再读取对象,确保数据的准确性。具体的探测和操作步骤如下:

这样会影响程序的性能吗?

会。据测试,最多百分之 4 的性能损耗。但这是 ZGC 并发转移的根底,为了升高 STW,设计者认为这点就义是可承受的。

ZGC 并发解决算法

ZGC 并发解决算法利用全局空间视图的切换和对象地址视图的切换,联合 SATB 算法实现了高效的并发。

以上所有的铺垫,都是为了讲清楚 ZGC 的并发解决算法,在一些博文上,都说染色指针和读屏障是 ZGC 的外围,但都没有讲清楚两者是如何在算法外面被利用的,我认为,ZGC 的并发解决算法才是 ZGC 的外围,染色指针和读屏障只不过是为算法服务而已。

ZGC 的并发解决算法三个阶段的全局视图切换如下:

  • 初始化阶段:ZGC 初始化之后,整个内存空间的地址视图被设置为 Remapped
  • 标记阶段:当进入标记阶段时的视图转变为 Marked0(以下皆简称 M0)或者 Marked1(以下皆简称 M1)
  • 转移阶段:从标记阶段完结进入转移阶段时的视图再次设置为 Remapped

标记阶段

标记阶段全局视图切换到 M0 视图。因为应用程序和标记线程并发执行,那么对象的拜访可能来自标记线程和应用程序线程。

在标记阶段完结之后,对象的地址视图要么是 M0,要么是 Remapped。

  • 如果对象的地址视图是 M0,阐明对象是沉闷的;
  • 如果对象的地址视图是 Remapped,阐明对象是不沉闷的,即对象所应用的内存能够被回收。

当标记阶段完结后,ZGC 会把所有沉闷对象的地址存到 对象沉闷信息表,沉闷对象的地址视图都是 M0。

转移阶段

转移阶段切换到 Remapped 视图。因为应用程序和转移线程也是并发执行,那么对象的拜访可能来自转移线程和应用程序线程。

至此,ZGC 的一个垃圾回收周期中,并发标记和并发转移就完结了。

为何要设计 M0 和 M1

咱们提到在标记阶段存在两个地址视图 M0 和 M1,下面的算法过程显示只用到了一个地址视图,为什么设计成两个?简略地说是为了区别前一次标记和以后标记。

ZGC 是依照页面进行局部内存垃圾回收的,也就是说当对象所在的页面须要回收时,页面外面的对象须要被转移,如果页面不须要转移,页面外面的对象也就不须要转移。

如图,这个对象在第二次 GC 周期开始的时候,地址视图还是 M0。如果第二次 GC 的标记阶段还切到 M0 视图的话,就不能辨别出对象是沉闷的,还是上一次垃圾回收标记过的。这个时候,第二次 GC 周期的标记阶段切到 M1 视图的话就能够辨别了,此时这 3 个地址视图代表的含意是:

  • M1:本次垃圾回收中辨认的沉闷对象。
  • M0:前一次垃圾回收的标记阶段被标记过的沉闷对象,对象在转移阶段未被转移,然而在本次垃圾回收中被辨认为不沉闷对象。
  • Remapped:前一次垃圾回收的转移阶段产生转移的对象或者是被应用程序线程拜访的对象,然而在本次垃圾回收中被辨认为不沉闷对象。

当初,咱们能够答复“应用地址视图和染色指针有什么益处”这个问题了

应用地址视图和染色指针能够放慢标记和转移的速度。以前的垃圾回收器通过批改对象头的标记位来标记 GC 信息,这是有内存存取拜访的,而 ZGC 通过地址视图和染色指针技术,无需任何对象拜访,只须要设置地址中对应的标记位即可。这就是 ZGC 在标记和转移阶段速度更快的起因。

当 GC 信息不再存储在对象头上时而存在援用指针上时,当确定一个对象曾经无用的时候,能够立刻重用对应的内存空间,这是把 GC 信息放到对象头所做不到的。

ZGC 步骤

ZGC 采纳的是标记 - 复制算法,标记、转移和重定位阶段简直都是并发的,ZGC 垃圾回收周期如下图所示:

ZGC 只有三个 STW 阶段:初始标记 再标记 初始转移

其中,初始标记和初始转移别离都只须要扫描所有 GC Roots,其解决工夫和 GC Roots 的数量成正比,个别状况耗时十分短;

再标记阶段 STW 工夫很短,最多 1ms,超过 1ms 则再次进入并发标记阶段。即,ZGC 简直所有暂停都只依赖于 GC Roots 汇合大小,进展工夫不会随着堆的大小或者沉闷对象的大小而减少。与 ZGC 比照,G1 的转移阶段齐全 STW 的,且进展工夫随存活对象的大小减少而减少。

ZGC 的倒退

ZGC 诞生于 JDK11,通过一直的欠缺,JDK15 中的 ZGC 曾经不再是试验性质的了。

从只反对 Linux/x64,到当初反对多平台;从不反对指针压缩,到反对压缩类指针 …..

在 JDK16,ZGC 将反对并发线程栈扫描(Concurrent Thread Stack Scanning),依据 SPECjbb2015 测试 后果,实现并发线程栈扫描之后,ZGC 的 STW 工夫又能升高一个数量级,进展工夫将进入毫秒时代。

ZGC 未然是一款优良的垃圾收集器了,它借鉴了 Pauseless GC,也仿佛在朝着 C4 GC 的方向倒退——引入分代思维。

Oracle 的致力,让咱们开发者看到了商用级别的 GC“飞入寻常百姓家”的心愿,随着 JDK 的倒退,我置信在将来的某一天,JVM 调优这种反人类的操作将不复存在,底层的 GC 会自适应各种状况主动优化。

ZGC 的确是 Java 的最前沿的技术,但在 G1 都没有遍及的明天,议论 ZGC 仿佛为时过早。但兴许咱们探讨的不是 ZGC,而是 ZGC 背地的设计思路。

心愿你能有所播种!

写在最初

为了对每一篇收回去的文章负责,力求精确,我个别是参考官网文档和业界权威的书籍,有些时候,还须要看一些论文,看一部分源代码。而官网文档和论文个别都是英文,对于一个英语四级只考了 456 分的人来说,十分艰巨,整个过程都是谷歌翻译和有道词典陪伴着我的。因为一些专业术语翻译的不够精确,还须要英文和翻译对照缓缓了解。

但即便这样,也难免会有纰漏,如果你发现了,欢送提出,我会对其修改。

你的正反馈对我来说十分重要,点个赞,点个再看,点个关注都是对我最大的反对!

谢谢您的浏览,咱们下期再见!

正文完
 0