❝扫描下方二维码或者微信搜索公众号
❞菜鸟飞呀飞
,即可关注微信公众号,阅读更多Spring源码分析
、Java并发编程
、Netty源码系列
、MySQL工作原理
和JVM专题系列
文章。
接上一篇文章JVM 系列之垃圾回收器(下篇)——Shenandoah 垃圾回收器,本文接下来介绍一款最前沿的垃圾回收器:ZGC。
ZGC 全称为 Z Garbage Collector,与 Shenandoah 一样,ZGC 也是一款在保证吞吐量的情况下,追求低延时的垃圾回收器。ZGC 是目前垃圾回收器中最前沿的技术,可惜的是目前 ZGC 还没有被正式使用,一直处于实验状态(Experiment)。从 JDK11 开始,被加入到了 OpenJDK 中,到目前 2020 年 4 月份发布的最新 Oracle JDK14 中,ZGC 依旧处于实验状态。
❝可以通过添加 JVM 参数:-XX:+UnlockExperimentalVMOptions 进行解锁实验状态。
❞
ZGC 依旧沿用了 G1 和 Shenandoah 中的 Region 内存布局,以及局部为复制算法,整体为标记-压缩(整理)算法来进行垃圾回收,但是 ZGC 与它们又有很大的区别,ZGC 到目前为止都不支持分代收集,也就是说不再区分新生代和老年代。且采用的 Region 分区也与 G1、Shenandoah 大不相同,ZGC 中的 Region 分区具有动态性:动态创建和销毁、大小也不固定。
另外,ZGC 采用了读屏障、染色指针、内存多重映射等技术来实现在整个垃圾回收过程中,「几乎」都是并发执行的(注意:这里使用的是「几乎」来形容,也就是说还是存在停顿过程),这使得 ZGC 造成的停顿时间可以控制在 10 毫秒以内的愿望得以实现。
在 G1 和 Shenandoah 中,Region 的大小都是固定的,每一个 Region 区域的大小也都是一样的,当 JVM 启动以后,Region 的大小就确定了,不可再改变。然而在 ZGC 中,虽然也沿用了基于 Region 的内存布局,但是 ZGC 中每个 Region 的大小可以不一样。在 ZGC 中,Region 可以分为三类:小型 Region、中型 Region、大型 Region。
ZGC 为了追求低延时,因此在整个回收过程中大部分阶段都是并发执行的,其中就包括并发整理阶段。在 G1 中,在 Region 回收整理阶段,需要暂停用户线程;在 Shenandoah 中,Region 的回收阶段,Shenandoah 采用了读屏障、写屏障、转发指针来实现 Region 的并发整理过程;而在 ZGC 中则是使用了「读屏障」以及「染色指针」来实现并发整理过程,那么什么是染色指针呢?
染色指针实际上就是对象的引用地址,在 64 位的机器中,对象引用地址的长度是 64bit,这 64bit 中,不是所有 bit 都表示对象的地址,而是仅有一部分 bit 表示对象的地址,具体是多少 bit 来表示对象的地址,这与「具体的操作系统有关系」。
例如在 64 位的 linux 下,用 46bit 来表示物理地址空间,在 64 位的 windows 下,则用 44bit 来表示物理地址空间。以 64 位 linux 系统为例,用 46bit 来表示对象的物理地址,剩下 18bit 则空闲,不代表任何含义,可能会在未来被使用。而在 ZGC 中,将 46bit 进行了进一步的「压榨」,只用 42bit 来表示真实的物理地址空间,「另外 4bit 被用来存储与对象相关的标志位信息」,如:三色标记状态、是否被移动、是否只能通过 finalize()方法被访问。
这就是染色指针,总结来说就是:「染色指针就是对象的引用地址,只不过引用地址中,有 4 个 bit 位表示了和对象相关的特殊标志位信息」。示意图如下。
看到这里我们知道了 ZGC 通过染色指针可以知道对象是否被标记、是否被移动过,那么在 HotSpot 虚拟机中,其他几种垃圾回收器是如何来标记对象的呢?
实际上,有的垃圾回收器将对象的标记信息存放在了对象头中(「对象头中还会存放锁信息、对象的哈希码、对象年龄等信息」),例如:Serial 垃圾回收器。有的垃圾回收器则会采用单独的数据结构来存放对象的标记信息,如 G1 和 Shenandoah 垃圾回收器则是采用了一个被称为 BitMap 的结构来存放标记信息。
现在思考一个问题:在 Shenandoah 中,并发清除阶段,当 Region 中的存活被移动到新 Region 中后,当用户线程来访问旧对象时,可以通过转发指针来让用户线程读取到新对象。那么在 ZGC 的并发清除阶段,当存活的对象被移动后,仅仅只是修改了旧对象染色指针中的标志位,那么用户线程访问到旧对象时,又是如何被转发到新对象中的?
答案就是「转发列表+内存多重映射技术」。ZGC 会为 Region 维护一份转发列表,当对象被移动时,ZGC 会更新转发列表,当用户线程访问旧对象时,会使用内存多重映射技术(多个虚拟地址映射到同一个物理地址上),让用户线程访问到新的对象,同时将旧的引用关系更新为新对象的地址,这种现象也被称之为染色指针的 「“自愈”」 现象。
染色指针是 ZGC 最显著的一个特点,ZGC 在垃圾回收方面的优异表现,可以说染色指针是最大功臣之一。那么使用染色指针到底有什么优点呢?
ZGC 在垃圾回收过程中,大方向上,和前面介绍的 G1、Shenandoah 类似,都会经过初始标记、并发标记、重新标记、Region 回收等阶段,但在部分阶段的实现细节上有着很大的区别。下面大致介绍一下 ZGC 的垃圾回收过程。
相比其他的垃圾回收器,ZGC 最大的优点就是「低延迟」了,它可以在任意堆内存大小下,将停顿时间控制在 10 毫秒以内,同时它的吞吐量也非常的高。
但是 ZGC 也存在缺点:目前为止,「不支持分代收集」。虽然 ZGC 可以在忽略堆内存大小的情况下,将停顿时间控制在 10 毫秒以内,但是如果堆内存非常大,完成一次垃圾回收可能需要 10 分钟(停顿时间仍然在 10 毫秒以内),如果此时系统分配新对象的速率特别大,在 10 分钟之内产生了很多新对象,而这些对象都是朝生夕灭的,而 ZGC 本次垃圾回收是无法回收这些垃圾的,那么这些对象就全是浮动垃圾。如果 ZGC 每次回收的内存又特别少,也就是说垃圾回收的速度跟不上对象的分配速度,那最终的后果就是内存被逐渐耗尽。那么碰到这种情况,解决的办法就是扩大堆内存,但是这种方法治标不治本,最终内存还是会被消耗完。最根本的解决方案还是得支持分代回收,对于那些朝生夕灭的对象,采用专门的回收方式。
文中一直强调 ZGC 的性能非常优秀,那么 ZGC 到底有多优秀呢?从《深入理解 Java 虚拟机》第三版)一书中,截取了几张图,从图表上感受一下 ZGC 的强悍吧。 图中将 ZGC 与 Parallel Scavenge、G1 分别从吞吐量和低延时两方面做了对比。在最大吞吐量的测试中,可以看到 ZGC 仅仅只是「略逊于」以吞吐量优先的 Parallel Scavenge,直接超过了 G1。
如果将停顿时间控制在某个值以内,ZGC 的吞吐量表现直接超过了 Parallel Scavenge 和 G1。
而在低延时性能测试中,ZGC 简直就是碾压 Parallel Scavenge 和 G1,无论是在平均停顿,还是 95%停顿、99%停顿、99.9%停顿,亦或是最大停顿,ZGC 的表现都是最优的,都控制在 10 毫秒以内。在测试结果图中,由于 ZGC 的表现实在是太优异了,以至于在纵坐标上都无法看到数据,因此将纵坐标换算为对数值来作图,如下。
ZGC 是目前性能表现最好的垃圾回收器,也是最前沿的垃圾回收器技术,可惜的是到目前为止,最新的 JDK14 中,ZGC 依旧还是处于「实验状态」。
ZGC 采用了「读屏障、染色指针以及内存多重映射」等技术,使得它能在任何大小的堆内存下,均能将停顿时间控制在 10 毫秒以内,这是一个令人震惊的、具有革命性的垃圾回收器,这必将是未来最主流的垃圾回收器。
本文使用 mdnice 排版