“本文基于 OpenJDK 17”
一、ZGC 概述
Z Garbage Collector,也称为ZGC,在 jdk 11 中引入的一种可扩展的低延迟垃圾收集器,在 jdk 15 中发布稳定版。在旨在满足以下目标:
-
< 1ms 最大暂停时间(jdk < 16 是 10ms,jdk >=16 是 <1ms )
-
STW 时间不会随着堆、或活跃对象的大小而增加
-
适用内存大小从 8MB 到16TB 的堆
从设计目标来看,我们知道ZGC适用于大内存低延迟服务的内存管理和回收。ZGC 的核心是一个并发垃圾收集器,这意味着所有繁重的工作都在Java 线程继续执行的同时完成。这极大的降低了GC 对应用程序响应时间的影响。
ZGC 特征: 并发、基于region、压缩、NUMA 感知、染色指针、读屏障等等
二、传统垃圾回收
我们在开发 Java 程序时,并不需要显示释放内存,Java 的垃圾回收器会自动帮我们回收。GC 会自动监测对象引用,并释放不可达的对象。GC 需要监测堆内存中对象的状态,如果一个对象不可达,GC 就可以考虑回收这个对象。
很多低延迟高可用Java服务的系统可用性经常受GC停顿的困扰。GC停顿指垃圾回收期间STW(Stop The World),当STW时,所有应用线程停止活动,等待GC停顿结束。以微服务服务为例,部分上游业务要求下游服务65ms内返回结果,并且可用性要达到99.99%。但因为GC停顿,我们未能达到上述可用性目标。当时使用的是CMS垃圾回收器,单次Young GC 40ms,一分钟10次,接口平均响应时间30ms。通过计算可知,有(40ms + 30ms) * 10次 / 60000ms = 1.12%的请求的响应时间会增加0 ~ 40ms不等,其中30ms * 10次 / 60000ms = 0.5%的请求响应时间会增加40ms。可见,GC停顿对响应时间的影响较大。为了降低GC停顿对系统可用性的影响,我们从降低单次GC时间和降低GC频率两个角度出发进行了调优,还测试过G1垃圾回收器,但这三项措施均未能降低GC对服务可用性的影响。
三、CMS 与 G1 停顿时间瓶颈
CMS新生代的Young GC、G1和ZGC都基于标记-复制算法,但算法具体实现的不同就导致了巨大的性能差异。
标记-复制算法应用在CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和G1垃圾回收器中。标记-复制算法可以分为三个阶段:
-
标记阶段,即从GC Roots集合开始,标记活跃对象;
-
转移阶段,即把活跃对象复制到新的内存地址上;
-
重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。
下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示:
会STW 的阶段 | 描述 |
标记阶段停顿分析 |
|
清理阶段停顿分析 |
|
复制阶段停顿分析 |
|
四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。
ZGC 就是在该阶段实现了并发,通过染色指针+读屏障来解决对象转移过程中的精确定位问题。
四、ZGC 原理
与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于1ms目标的最关键原因。
ZGC垃圾回收周期如下图所示:
会STW 的阶段 | 描述 |
初始标记 | 只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比。 |
再标记 | 再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。 |
初始转移 | 只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比, |
ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。
五、ZGC 关键技术
5.1 ZGC 内存布局
ZGC 收集器是一款基于 Region 内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
ZGC 的内存布局说起。与 Shenandoah 和 G1一样,ZGC 也采用基于 Region 的堆内存布局,但与它们不同的是 , ZGC 的 Region 具 有 动 态 性 (动态创建和销毁 , 以及动态的区域容量大小)。 在 x64硬件平台下 , ZGC 的 Region 可以具有大、中、小三类容量(如下图所示):
-
小型 Region (Small Region ):容量固定为 2M, 存放小于 256K 的对象。
-
中型 Region (Medium Region):容量固定为 32M,放置大于等于256K但小于4M的对象。
-
大型 Region (Large Region): 容量不固定,可以动态变化,但必须为2MB 的整数倍,用于放置 4MB或以上的大对象。
5.2 NUMA 支持
NUMA 对应的有 NMA 、UMA 即 Uniform Memory Access Architecture, NUMA 就是 Non Uniform Memory Access Architecture. UMA 表示内存只有一块,所有的 CUU 都要去访问这些内存,那么会存在竞争问题(竞争内存总线访问权),有竞争就要去加锁,有锁效率就会受到影响,而且 CPU 核心数越多,竞争就越激烈。 NUMA 的话每个 CPU 对应有一个内存块,且这块内存在主板上离这个 CPU 是最近的,每个 CPU 优最先访问这块内存,那效率就自然提高了。
服务器的 NUMA 架构在中大型系统上非常流行,也就是高性能的解决方案,尤其在系统延迟方面表现非常优秀,ZGC 是能自动感知 NUMA 架构并且充分利用 NUMA 架构的特征。
5.3 染色指针
“染色指针是一种将信息存储在指针中的技术。”
ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示:
每个对象有一个64位指针,这64位被分为:
-
16位:预留给以后使用;
-
1位: Finalizable标识;
-
1位:Remapped标识;
-
1位:Marked1标识;
-
1位: Marked0标识;
-
44位:对象的地址(所以它可以支持2^44=16T内存);
5.4 多重映射地址
第一种解释描述
不同的虚拟机内存到物理内存的转换关系可以在硬件层面,操作系统层面或者软件层面来实现。 在 Linux 平台上 ZGC 采用了多重映射(Mult-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,这是一种多对一映射,一位着 ZGC 在虚拟内中看到的地址空间要比实际的堆内存容量来得更大。把着色指针中的标志位看作是地址分段符,那只要将这些不同的地址分段符都映射到同一个物理内存空间,经过多重映射转换后,就可以直接使用染色指针进行寻址了,如下图所示:
多重映射技术确实可能带来一些诸如复制大对象时会更容易这样额外的好处,但是从源头上来说,ZGC 的多重映射只是采用着色指针的衍生品,并不是为了专门的为实现其他某种特征需求而做的。
其中,[0~8TB) 对应 Java 堆,[8TB ~ 12TB) 称为 M0 地址空间,[12TB ~ 16TB) 称为 M1 地址空间,[16TB ~ 18TB) 预留未使用,[18TB ~ 24TB) 称为 Remapped 空间。
当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC 同时会为该对象在 M0、M1 和 Remapped 地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC 之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低 GC 停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。
第二种解释描述
ZGC参照操作系统中的虚拟地址和物理地址,设计了一套内存和地址的多重映射关系。
我们先看一下这个例子:
你在你爸爸妈妈眼中是儿子,在你女朋友眼中是男朋友。在全世界人面前就是最帅的人。你还有一个名字,但名字也只是你的一个代号,并不是你本人。
将这个关系画一张映射图表示:
假如你的名字是全世界唯一的,通过“你的名字”、“你爸爸的儿子”、“你女朋友的男朋友”,“世界上最帅的人”最后定位到的都是你本人。
现在我们再来看看ZGC的内存映射。
ZGC为了能高效、灵活地管理内存,实现了两级内存管理:虚拟内存和物理内存,并且实现了物理内存和虚拟内存的映射关系。
当应用程序创建对象时,首先在堆空间申请一个虚拟地址,ZGC同时会为该对象在Marked0、Marked1和Remapped三个视图空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址。
图中的Marked0、Marked1和Remapped三个视图是什么意思呢?
这是ZGC的三个视图空间,在ZGC中这三个空间在同一时间点有且仅有一个空间有效。
对照上面的例子,这三个视图分别对应的就是"你爸爸眼中",“你女朋友的眼中”,“全世界人眼中”。
而三个视图里面的地址,都是虚拟地址。对应的是“你爸爸眼中的儿子”,“你女朋友眼中的男朋友”......
最后,这些虚地址都能映射到同一个物理地址,这个物理地址对应上面例子中的“你本人”。
用一段简单的Java代码表示这种关系:
ZGC为什么这么设计呢?这就是ZGC的高明之处,利用虚拟空间换时间,这三个空间的切换是由垃圾回收的不同阶段触发的,通过限定三个空间在同一时间点有且仅有一个空间有效,高效的完成了GC过程的并发操作,具体实现会在后面讲ZGC并发处理算法的部分详细描述。
5.5 读屏障
“读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。”
读屏障示例:
Object o = obj.FieldA // 从堆中读取引用,需要加入屏障 <Load barrier> Object p = o // 无需加入屏障,因为不是从堆中读取引用 o.dosomething() // 无需加入屏障,因为不是从堆中读取引用 int i = obj.FieldB //无需加入屏障,因为不是对象引用
在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障,写屏障是在对象引用赋值时候的 AOP,而读屏障是在读取引用时的 AOP。
ZGC中读屏障的代码作用:
GC线程和应用线程是并发执行的,所以存在应用线程去A对象内部的引用所指向的对象B的时候,这个对象B正在被GC线程移动或者其他操作,加上读屏障之后,应用线程会去探测对象B是否被GC线程操作,然后等待操作完成再读取对象,确保数据的准确性。
具体的探测和操作步骤如下:
第二个例子:
Ojbect a = obj.foo
不仅 a 能得到最新的引用地址,obj.foo 也会被更新,这样下次访问的时候一切都是正常的,就没有消耗了。
下图展示了读屏障的效果,其实就是转移的时候找地方记一下即 forwardingTable,然后读的时候触发引用的修正。
这种也称之为“自愈”,不仅赋值的引用时最新的,自身引用也修正了。
读屏障的存在会导致ZGC的应用的吞吐量会变低。官方的测试数据是需要多出额外4%的开销:
染色指针和读屏障是 ZGC 能实现并发转移的关键所在。
5.6 ZGC 并发处理算法
ZGC并发处理算法利用全局空间视图的切换和对象地址视图的切换,结合SATB算法实现了高效的并发。
ZGC并发处理算法利用全局空间视图的切换和对象地址视图的切换,结合SATB算法实现了高效的并发。
以上所有的铺垫,都是为了讲清楚ZGC的并发处理算法,在一些博文上,都说染色指针和读屏障是ZGC的核心,但都没有讲清楚两者是如何在算法里面被利用的,我认为,ZGC的并发处理算法才是ZGC的核心,染色指针和读屏障只不过是为算法服务而已。
ZGC 并发处理算法三个阶段的全局视图切换如下:
-
初始化阶段:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped
-
并发标记阶段:当进入标记阶段时的视图转变为Marked0(以下皆简称M0)或者Marked1(以下皆简称M1) 。第一次进入标记阶段时视图为 M0,如果对象被 GC 标记线程或者应用线程访问过,那么就将对象的地址视图从 Remapped 调整为 M0。所以,在标记阶段结束之后,对象的地址要么是 M0 视图,要么是 Remapped。如果对象的地址是 M0 视图,那么说明对象是活跃的;如果对象的地址是 Remapped 视图,说明对象是不活跃的。
-
并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为 Remapped。如果对象被 GC 转移线程或者应用线程访问过,那么就将对象的地址视图从 M0 调整为 Remapped。
标记阶段
标记阶段全局视图切换到M0视图。因为应用程序和标记线程并发执行,那么对象的访问可能来自标记线程和应用程序线程。
在标记阶段结束之后,对象的地址视图要么是M0,要么是Remapped。
-
如果对象的地址视图是M0,说明对象是活跃的;
-
如果对象的地址视图是Remapped,说明对象是不活跃的,即对象所使用的内存可以被回收。
当标记阶段结束后,ZGC会把所有活跃对象的地址存到对象活跃信息表,活跃对象的地址视图都是M0。
转移阶段
转移阶段切换到Remapped视图。因为应用程序和转移线程也是并发执行,那么对象的访问可能来自转移线程和应用程序线程。
对象视图在 初始化阶段、标记阶段、转移阶段 扭转图:
至此,ZGC的一个垃圾回收周期中,并发标记和并发转移就结束了。
为何要设计M0和M1
在标记阶段存在两个地址视图 M0 和 M1,上面的过程显示只用了一个地址视图。之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为 M1,而非 M0。
ZGC是按照页面进行部分内存垃圾回收的,也就是说当对象所在的页面需要回收时,页面里面的对象需要被转移,如果页面不需要转移,页面里面的对象也就不需要转移
如图,这个对象在第二次GC周期开始的时候,地址视图还是M0。如果第二次GC的标记阶段还切到M0视图的话,就不能区分出对象是活跃的,还是上一次垃圾回收标记过的。这个时候,第二次GC周期的标记阶段切到M1视图的话就可以区分了,此时这3个地址视图代表的含义是:
-
M1:本次垃圾回收中识别的活跃对象。
-
M0:前一次垃圾回收的标记阶段被标记过的活跃对象,对象在转移阶段未被转移,但是在本次垃圾回收中被识别为不活跃对象。
-
Remapped:前一次垃圾回收的转移阶段发生转移的对象或者是被应用程序线程访问的对象,但是在本次垃圾回收中被识别为不活跃对象。
使用地址视图和染色指针有什么好处:
-
使用地址视图和染色指针可以加快标记和转移的速度。以前的垃圾回收器通过修改对象头的标记位来标记GC信息,这是有内存存取访问的,而ZGC通过地址视图和染色指针技术,无需任何对象访问,只需要设置地址中对应的标志位即可。这就是ZGC在标记和转移阶段速度更快的原因。
-
当GC信息不再存储在对象头上时而存在引用指针上时,当确定一个对象已经无用的时候,可以立即重用对应的内存空间,这是把GC信息放到对象头所做不到的。
染色指针和读屏障技术不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在 ZGC 中,只需要设置指针地址的第 42~45 位即可,并且因为是寄存器访问,所以速度比访问内存更快。
5.7 ZGC 垃圾回收详细流程解析
Zgc 垃圾回收分三个大的阶段,可拆分为10个步骤。
步骤 | 作业 | 是否会STW |
1、初始化 | 仅标记CG ROOTS直接可达的对象,压到标记栈中 | 会 |
2、并发标记 | 根据初始标记的对象开始并发遍历对象图,还会统计每个 region 的存活对象的数量。 标记栈其实只有一个,但是并发标记的线程有多个。为了减少之间的竞争每个线程其实会分到不同的标记带来执行。你就理解为标记栈被分割为好几块,每个线程负责其中的一块进行遍历标记对象,就和1.7 Hashmap 的segment 一样。那肯定有的线程标记的快,有的标记的慢,那么先空闲下来的线程会去窃取别人的任务来执行,从而实现负载均衡。 看到这有没有想到啥?没错就是 ForkJoinPool 的工作窃取机制! | |
3、再标记阶段 | 因为并发阶段应用线程还是在运行的,所以会修改对象的引用导致漏标的情况。 因此需要个再标记阶段来标记漏标的那些对象。如果这个阶段执行的时间过长,就会再次进入到并发标记阶段,因为 ZGC 的目标就是低延迟,所以一有高延迟的苗头就得扼制。 | 会 |
4、非强引用并发标记和引用并发处理 | 就是上一步非强根的遍历,然后引用就软引用、弱引用、虚引用的一些处理。 这个阶段是并发 | |
5、重置转移集 | forwardingTable 就是个映射集,你可以理解为 key 就是对象转移前的地址,value 是对象转移后的地址。 不过这个映射集在标记阶段已经用了,也就是说标记的时候已经重定位完了,所以现在没用了。 但新一轮的垃圾回收需要还是要用到这个映射集的。 因此在这个阶段对那些转移分区的地址映射集做一个复位的操作。 | |
6、回收无效分区 | 回收那些物理内存已经被释放的无效的虚拟内存页面。 就是在内存紧张的时候会释放物理内存,如果同时释放虚拟空间的话也不能释放分区,因为分区需要在新一轮标记完成之后才能释放。 所以就会有无效的虚拟内存页面存在,在这个阶段回收。 | |
7、选择待回收的分区 | 这和 G1 一样,因为会有很多可以回收的分区,会筛选垃圾较多的分区,来作为这次回收的分区集合。 | |
8、初始化待转移集合的转移表 | 这一步就是初始化待回收的分区的 forwardingTable。 | |
9、初始转移 | 这个阶段其实就是从根集合出发,如果对象在转移的分区集合中,则在新的分区分配对象空间。 如果不在转移分区集合中,则将对象标记为 Remapped。 注意这个阶段是 STW,只转移根直接可达的对象。 | 会 |
10、并发转移 | 从上一步转移的对象开始遍历做并发转移。 这步很关键,G1 的转移对象需要STW,而ZGC不需要,所有延迟会低很多 |
5.8 ZGC 触发时机
相比于CMS和G1的GC触发机制,ZGC的GC触发机制有很大不同。ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。
ZGC有多种GC触发机制,总结如下:
-
阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
-
基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
-
基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
-
主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
-
预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
-
外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
-
元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。
5.9 ZGC GC 日志
一次完整的GC过程,需要注意的点已在图中标出。
GC日志中每一行都注明了GC过程中的信息,关键信息如下:
-
Start:开始GC,并标明的GC触发的原因。上图中触发原因是自适应算法。
-
Phase-Pause Mark Start:初始标记,会STW。
-
Phase-Pause Mark End:再次标记,会STW。
-
Phase-Pause Relocate Start:初始转移,会STW。
-
Heap信息:记录了GC过程中Mark、Relocate前后的堆大小变化状况。High和Low记录了其中的最大值和最小值,我们一般关注High中Used的值,如果达到100%,在GC过程中一定存在内存分配不足的情况,需要调整GC的触发时机,更早或者更快地进行GC。
-
GC信息统计:可以定时的打印垃圾收集信息,观察10秒内、10分钟内、10个小时内,从启动到现在的所有统计信息。利用这些统计信息,可以排查定位一些异常点。
日志中内容较多,关键点已用红线标出,含义较好理解,更详细的解释大家可以自行在网上查阅资料。
5.10 ZGC 停顿原因
我们在实战过程中共发现了6种使程序停顿的场景,分别如下:
-
GC时,初始标记:日志中Pause Mark Start。
-
GC时,再标记:日志中Pause Mark End。
-
GC时,初始转移:日志中Pause Relocate Start。
-
内存分配阻塞:当内存不足时线程会阻塞等待GC完成,关键字是”Allocation Stall”。
-
安全点:所有线程进入到安全点后才能进行GC,ZGC定期进入安全点判断是否需要GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。
-
dump线程、内存:比如jstack、jmap命令。
5.11 ZGC 参数
通用GC参数 | ZGC 参数 | ZGC 诊断工具(-XX:+UnlockDiagnosticVMOptions) |
-XX:MinHeapSize, -Xms -XX:InitialHeapSize, -Xms -XX:MaxHeapSize, -Xmx -XX:SoftMaxHeapSize -XX:ConcGCThreads -XX:ParallelGCThreads -XX:UseDynamicNumberOfGCThreads -XX:UseLargePages -XX:UseTransparentHugePages -XX:UseNUMA -XX:SoftRefLRUPolicyMSPerMB -XX:AllocateHeapAt | -XX:ZAllocationSpikeTolerance -XX:ZCollectionInterval -XX:ZFragmentationLimit -XX:ZMarkStackSpaceLimit -XX:ZProactive -XX:ZUncommit -XX:ZUncommitDelay -Xlog -XX:+AlwaysPreTouch | -XX:ZStatisticsInterval -XX:ZVerifyForwarding -XX:ZVerifyMarking -XX:ZVerifyObjects -XX:ZVerifyRoots -XX:ZVerifyViews |
ConcGCThreads
ZGC是一个并发垃圾收集器,那么并发GC线程数就非常重要了。如果设置并发GC线程数越多,意味着应用线程数就会越少,这肯定是非常不利于应用系统稳定运行的。这个参数ZGC能自动设置,如果没有十足的把握。最好不要设置这个参数。
ParallelGCThreads
STW阶段使用线程数,默认是总核数的60%。这是个并行线程数,与上一个参数ConcGCThreads有所不同,ConcGCThreads表示GC线程和应用线程「并发」执行时GC线程数量。而ParallelGCThreads表示GC时STW阶段的「并行」GC线程数量(例如第一阶段的Root扫描),这时候只有GC线程,没有应用线程。
UseDynamicNumberOfGCThreads
从 JDK 17 开始,ZGC 现在支持 -XX:+UseDynamicNumberOfGCThreads 并尝试使用尽可能少的线程,但要有足够的线程以保持其创建速率收集垃圾。这有助于避免使用比需要更多的 CPU 时间,这反过来将使更多的 CPU 时间可用于 Java 线程。 另请注意,启用此功能后,-XX:ConcGCThreads 的含义从“使用这么多线程”变为“最多使用这么多线程”。但是除非你有非常规的工作负载,否则你通常不需要摆弄 -XX:ConcGCThreads。 ZGC 的启发式算法会根据您运行的系统的大小为您选择合适的最大线程数。
UseNUMA
ZGC默认是开启支持NUMA的,不过,如果JVM探测到系统绑定的是CPU子集,就会自动禁用NUMA。我们可以通过参数-XX:+UseNUMA显示启动,或者通过参数-XX:-UseNUMA显示禁用。如果运行在NUMA服务器上,并且设置-XX:+UseNUMA,那对性能提升是显而易见的。
UseLargePages
配置ZGC使用large page通常就会得到更好的性能,比如在吞吐量、延迟、启动时间等方面。而且没有明显的缺点,除了配置过程复杂一点。因为它需要root权限,这也是默认并没有开启使用large page的原因。
ZAllocationSpikeTolerance
ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。
ZCollectionInterval
ZGC发生的最小时间间隔,单位秒
ZProactive
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否启用主动回收,默认开启,这里的配置表示关闭。
ZUncommit
掌握这个参数之前,我们先说一下JVM申请以及回收内存的行为。以前的垃圾回收器比如ParallelOldGC和CMS,只要JVM申请过的内存,即使发生了GC回收了很多内存空间,JVM也不会把这些内存归还给操作系统。这就会导致top命令中看到的RSS只会越来越高,而且一般都会超过Xmx的值
不过,默认情况下,ZGC是会把不再使用的内存归还给操作系统的。这对于那些比较注意内存占用情况的应用和服务器来说,是很有用的。这种行为可以通过JVM参数-XX:-ZUncommit关闭。不过,无论怎么归还,JVM至少会保留Xms参数指定的内存大小,这就是说,当Xmx和Xms一样大的时候,这个参数就不起作用了。
和这个参数一起起作用的还有另一个参数:-「XX:ZUncommitDelay=sec」,默认300秒。这个参数表示不再使用的内存最多延迟多长时间才会被归还给操作系统。因为不再使用的内存不应该立即归还给操作系统,这样会造成频繁的归还和申请行为,所以通过这个参数来控制不再使用的内存需要经过多久的时间才归还给操作系统
AlwaysPreTouch
当使用 -XX:+AlwaysPreTouch 时,你告诉 GC 在启动时接触堆(最多 -Xms 或 -XX:InitialHeapSize)。这将确保支持堆的内存页面是 1) 实际分配的和 2) 错误的。通过在启动时执行此操作,您可以避免稍后在应用程序运行并开始接触内存时承担此成本。对于某些应用程序来说,预接触堆可能是一个明智的选择,但一如既往,这是一种权衡,因为启动时间会延长。
在 JDK 14 之前,ZGC 仅使用单线程进行堆预接触。这意味着如果堆很大,预接触可能需要很长时间。现在,ZGC 使用多线程来完成这项工作,大大缩短了启动/预触摸时间。在具有 TB 内存的大型机器上,这种减少可以转化为以秒而不是分钟为单位的启动时间。
Xlog
-Xlog:设置GC日志中的内容、格式、位置以及每个日志的大小。
JDK9 之后已经不用 -xloggc,只用 -Xlog
-Xlog:safepoint,classhisto=trace,age,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m
六、ZGC 的发展
ZGC诞生于JDK11,经过不断的完善,JDK15中的ZGC已经不再是实验性质的了。JDK 16 并发线程栈扫描(Concurrent Thread Stack Scanning)将STW 时间再降低一个数量级进入亚毫秒时代, JDK17 ZGC 没有更新说明已经稳定了。
从只支持Linux/x64,到现在支持多平台;从不支持指针压缩,到支持压缩类指针.....ZGC迭代的速度非常快。
ZGC是一款优秀的垃圾收集器,它借鉴了Pauseless GC,也似乎在朝着C4 GC的方向发展——引入分代思想。
ZGC追求低停顿时间,并将此做到极致,虽然牺牲了一部分的性能,但完全可以接受。其中的染色指针技术和多重映射思想也值得我们学习。