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分的人来说,非常艰难,整个过程都是谷歌翻译和有道词典陪伴着我的。因为一些专业术语翻译的不够准确,还需要英文和翻译对照慢慢理解。

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

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

谢谢您的阅读,我们下期再见!

号称能将STW干掉1ms的Java垃圾收集器ZGC到底是个什么东西?的更多相关文章

  1. 面试官,不要再问我“Java 垃圾收集器”了

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...

  2. 面试官,不要再问我“Java 垃圾收集器”了(转载)

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在 面试过程中这个深度的问题涉及的比 ...

  3. 【Java】JVM(三)、Java垃圾收集器

    一.Minor GC.Major GC 和 Full GC Minor GC:清理新生代空间,当Eden空间不能分配时候引发Minor GC Major GC:清理老年代空间 Full GC:清理Ja ...

  4. Java垃圾收集器——Serial,Parallel,CMS,G1收集器概述

    1.概述 Java应用启动的时候,除了配置Xms以及Xmx参数(Xmx:InitialHeapSize, Xms:MaxHeapSize),还需要选择合适的垃圾收集器. 截止Jdk1.8,共提供了7款 ...

  5. Java垃圾收集器

    概述 说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物.事实上,GC的历史远远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用 ...

  6. 深入理解JVM : Java垃圾收集器

    如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现. Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商.不同版本的虚拟机所提供的垃圾收集器都可能会有很大差 ...

  7. Java垃圾收集器概述

    垃圾收集器的操作 查找未使用的对象,释放内存,并压缩堆,避免内存碎片 一个java程序,有执行应用程序逻辑的线程和执行GC的线程组.当GC跟踪对象引用,或在内存中移动对象,它必须确保应用程序线程没有使 ...

  8. 重读《深入理解Java虚拟机》二、Java如何分配和回收内存?Java垃圾收集器如何工作?

    线程私有的内存区域随用户线程的结束而回收,内存分配编译期已确定,内存分配和回收具有确定性.共享线程随虚拟机的启动.结束而建立和销毁,在运行期进行动态分配.垃圾收集器主要对共享内存区域(堆和方法区)进行 ...

  9. Java垃圾收集器——Parallel、G1收集器日志分析及性能调优示范

    开发过程中,经常需要对GC的垃圾收集器参数不断的进行动态调整,从而更充分的压榨机器性能,提升应用效率.本文将从常见的Parallel/G1垃圾收集器的GC日志着手,分析GC日志的具体含义,以及示范如何 ...

随机推荐

  1. 【eJOI2020】考试(dp & 树状数组优化)

    Description \(n\) 个正整数排成一列,每个位置 \(i\) 有一个初始值 \(A_i\) 以及目标值 \(B_i\). 一次操作可以选定一个区间 \([l, r]\),并将区间内所有数 ...

  2. AcWing 195. 骑士精神

    双向BFS (广搜) \(O(8 ^ 7)\) 看到没有双向BFS的题解我就过来了 这道题也可以用双向\(BFS\)来做,时间复杂度与\(IDA*\)不相上下. 双向\(BFS\)的实现有多种: 把初 ...

  3. dp斜率优化

    算法-dp斜率优化 前置知识: 凸包 斜率优化很玄学,凭空讲怎么也讲不好,所以放例题. [APIO2014]序列分割 [APIO2014]序列分割 给你一个长度为 \(n\) 的序列 \(a_1,a_ ...

  4. MySQL技术内幕InnoDB存储引擎(五)——索引及其相关算法

    索引概述 索引太多可能会降低运行性能,太少就会影响查询性能. 最开始就要在需要的地方添加索引. 常见的索引: B+树索引 全文索引 哈希索引 B+树索引 B+树 所有的叶子节点存放完整的数据,非叶子节 ...

  5. java和python的时间格式化区别

    java 和 python时间格式化区别 月份,java是M,python是m 分钟,java是m,python是M 年份,必须用yyyy,表示当天所在的年份,如果用YYYY,则表示当前周所在年份 j ...

  6. Spring 中常用的注解

    (1).用于注册bean对象的注解 1.1@Component: 作用: 调用无参构造创建一个bean对象,并把对象存入spring的Ioc容器,交由spring容器进行管理.相当于在xml中配置一个 ...

  7. JavaSE07-字符串常用API

    1.String 1.1 String类概述 String 类代表字符串,Java 程序中的所有字符串文字(例如"abc")都被实现为此类的实例.也就是说,Java 程序 中所有的 ...

  8. Shiro实现Basic认证

    前言 今天跟小伙伴们分享一个实战内容,使用Spring Boot+Shiro实现一个简单的Http认证. 场景是这样的,我们平时的工作中可能会对外提供一些接口,如果这些接口不做一些安全认证,什么人都可 ...

  9. jmeter性能测试-高并发分布式部署

    jmeter什么要做分布式部署? jmeter是运行在JVM虚拟机上的,当模拟大量并发时,对运行机器的性能/网络负载会很大. 此时就需要使用jmeter的分布式部署功能,实现多台被控机器同时并发访问被 ...

  10. 网络编程-python实现-TCP(1.1.3)

    @ 目录 1.TCP是什么 2.代码实现 1.TCP是什么 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的.可靠的.基于字节流的传输层通信协议,由I ...