@[TOC]
一、了解ZGC
1、ZGC出现背景
对于Java的项目来说,JVM进行垃圾回收会有一个很大的问题,就是STW(StopTheWorld)。
在很多业务场景中,STW时间太长是非常致命的,比如说手机系统(Android) 显示卡顿,通过对 GC 算法的不断演进,停顿时间控制在几个ms 级别;再比如说一些实时证券交易系统以及一些大数据平台,大规模部署的情况下,STW太久会造成很严重的影响。
为了满足不同的业务需求,Java 的 GC 算法也在不停迭代,对于特定的应用,选择其最适合的 GC 算法,才能更高效的帮助业务实现其业务目标。对于这些延迟敏感的应用来说,GC 停顿已经成为阻碍 Java 广泛应用的一大顽疾,需要更适合的 GC 算法以满足这些业务的需求。
近些年来,服务器的性能越来越强劲,各种应用可使用的堆内存也越来越大,常见的堆大小从 10G 到百 G
级别,部分机型甚至可以到达 TB
级别,在这类大堆应用上,传统的 GC,如 CMS、G1 的停顿时间也跟随着堆大小的增长而同步增加,即堆大小指数级增长时,停顿时间也会指数级增长。特别是当触发 Full GC 时,停顿可达分钟级别(百GB级别的堆)
。当业务应用需要提供高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此时 CMS、G1 等就无法满足业务的需求。
为满足当前应用对于超低停顿、并应对大堆和超大堆
带来的挑战,伴随着 2018 年发布的 JDK 11,A Scalable Low-Latency Garbage Collector - ZGC 应运而生。
2、ZGC介绍
ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求极致低延迟的垃圾收集器,它曾经设计目标包括:
- 支持TB量级的堆。我们生产环境的硬盘还没有上TB呢,这应该可以满足未来十年内,所有JAVA应用的需求了吧。
- 最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右,Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能做到这一点是因为它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。
- 奠定未来GC特性的基础。
- 最糟糕的情况下吞吐量会降低15%。这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容分分钟解决。
另外,Oracle官方提到了它最大的优点是:它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。
二、ZGC中的内存布局
1、ZGC内存布局
ZGC收集器是一款基于Region内存布局的, 暂时不设分代的
, 使用了读屏障、 颜色指针
等技术来实现可并发的标记-整理算法
的, 以低延迟为首要目标的一款垃圾收集器。
ZGC的Region可以具有如图3-19所示的大、 中、 小三类容量: 小型Region(Small Region)
: 容量固定为2MB, 用于放置小于256KB的小对象。 中型Region(Medium Region)
: 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。 大型Region(Large Region)
: 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作"大型Region", 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作, 用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂。
ZGC对于不同页面回收的策略也不同。简单地说,小Region优先回收;中Region和大Region则尽量不回收
。
标准大页(huge page)是Linux Kernel 2.6引入的,目的是通过使用大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。 Huge pages 有两种格式大小: 2MB 和 1GB , 2MB 页块大小适合用于 GB 大小的内存, 1GB 页块大小适合用于 TB 级别的内存; 2MB 是默认的页大小。 所以ZGC这么设置也是为了适应现代硬件架构的发展,提升性能。
2、NUMA-aware
在过去,对于X86架构的计算机,内存控制器还没有整合进CPU,所有对内存的访问都需要通过北桥芯片来完成。X86系统中的所有内存都可以通过CPU进行同等访问。任何CPU访问任何内存的速度是一致的,不必考虑不同内存地址之间的差异,这称为"统一内存访问"(Uniform Memory Access,UMA)。UMA系统的架构示意图如图所示。 在UMA中,各处理器与内存单元通过互联总线进行连接,各个CPU之间没有主从关系。之后的X86平台经历了一场从"拼频率"到"拼核心数"的转变,越来越多的核心被尽可能地塞进了同一块芯片上,各个核心对于内存带宽的争抢访问成为瓶颈,所以人们希望能够把CPU和内存集成在一个单元上(称Socket),这就是非统一内存访问(Non-Uniform Memory Access,NUMA)。很明显,在NUMA下,CPU访问本地存储器的速度比访问非本地存储器快一些。下图所示是支持NUMA处理器架构示意图。 ZGC是支持NUMA的(UMA即Uniform Memory Access Architecture,NUMA就是Non Uniform Memory Access Architecture),在进行小页面分配时会优先从本地内存分配,当不能分配时才会从远端的内存分配。对于中页面和大页面的分配,ZGC并没有要求从本地内存分配,而是直接交给操作系统,由操作系统找到一块能满足ZGC页面的空间。ZGC这样设计的目的在于,对于小页面,存放的都是小对象,从本地内存分配速度很快,且不会造成内存使用的不平衡,而中页面和大页面因为需要的空间大,如果也优先从本地内存分配,极易造成内存使用不均衡,反而影响性能。
UMA表示内存只有一块,所有CPU都去访问这一块内存,那么就会存在竞争问题(争夺内存总线访问权),有竞争就会有锁,有锁效率就会受到影响,而且CPU核心数越多,竞争就越激烈。NUMA的话每个CPU对应有一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存,那效率自然就提高了。
服务器的NUMA架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀。ZGC是能自动感知NUMA架构并充分利用NUMA架构特性的。
三、ZGC核心概念与运行流程
1、指针着色技术(Color Pointers)
(1)验证
颜色指针可以说是ZGC的核心概念。因为他在指针中借了几个位出来做事情,所以它必须要求在64位的机器上才可以工作。并且因为要求64位的指针,也就不能支持压缩指针。
ZGC中低42位表示使用中的堆空间
ZGC借几位高位来做GC相关的事情(快速实现垃圾回收中的并发标记、转移和重定位等)
我们通过一个例子演示Linux多视图映射。Linux中主要通过系统函数mmap完成视图映射。多个视图映射就是多次调用mmap函数,多次调用的返回结果就是不同的虚拟地址。示例代码如下:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdint.h>
int main()
{
//创建一个共享内存的文件描述符
int fd = shm_open("/example", O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd == -1) return 0;
//防止资源泄露,需要删除。执行之后共享对象仍然存活,但是不能通过名字访问
shm_unlink("/example");
//将共享内存对象的大小设置为4字节
size_t size = sizeof(uint32_t);
ftruncate(fd, size);
//3次调用mmap,把一个共享内存对象映射到3个虚拟地址上
int prot = PROT_READ | PROT_WRITE;
uint32_t *remapped = mmap(NULL, size, prot, MAP_SHARED, fd, 0);
uint32_t *m0 = mmap(NULL, size, prot, MAP_SHARED, fd, 0);
uint32_t *m1 = mmap(NULL, size, prot, MAP_SHARED, fd, 0);
//关闭文件描述符
close(fd);
//测试,通过一个虚拟地址设置数据,3个虚拟地址得到相同的数据
*remapped = 0xdeafbeef;
printf("48bit of remapped is: %p, value of 32bit is: 0x%x\n", remapped, *remapped);
printf("48bit of m0 is: %p, value of 32bit is: 0x%x\n", m0, *m0);
printf("48bit of m1 is: %p, value of 32bit is: 0x%x\n", m1, *m1);
return 0;
}
在Linux上通过gcc编译后运行文件,得到的执行文件: gcc -lrt -o mapping mapping.c 然后执行下,我们来看下执行结果 从结果我们可以发现,3个变量对应3个不同的虚拟地址。 **实地址:(32位指针)**是:0xdeafbeef <一位16进制代表4位二进制> 虚地址:(48位指针): 0x7f93aef8e000<虚地址remapped> 0x7f93aef8d000<虚地址m0> 0x7f93aef8c000<虚地址m1> 但是因为它们都是通过mmap映射同一个内存共享对象,所以它们的物理地址是一样的,并且它们的值都是0xdeafbeef。
(2)ZGC中的染色指针
以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。 每个对象有一个64位指针,这64位被分为: 18位:预留给以后使用; 1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问; 1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合); 1位:Marked1标识; 1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC; 42位:对象的地址(所以它可以支持2^42=4T内存):
为什么有2个mark标记? 每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。 GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。 GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。 通过对配置ZGC后对象指针分析我们可知,对象指针必须是64位,那么ZGC就无法支持32位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是32位)。
颜色指针的三大优势: 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。 颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。
2、读屏障
之前的GC都是采用Write Barrier,这次ZGC采用了完全不同的方案读屏障,这个是ZGC一个非常重要的特性。
在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个Load Barriers。
那么我们该如何理解它呢?看下面的代码,第一行代码我们尝试读取堆中的一个对象引用obj.fieldA并赋给引用o(fieldA也是一个对象时才会加上读屏障)。如果这时候对象在GC时被移动了,接下来JVM就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针"修正"到原本的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW。
那么,JVM是如何判断对象被移动过呢?就是利用上面提到的颜色指针,如果指针是Bad Color,那么程序还不能往下执行,需要「slow path」,修正指针;如果指针是Good Color,那么正常往下执行即可: ❝ 这个动作是不是非常像JDK并发中用到的CAS自旋?读取的值发现已经失效了,需要重新读取。而ZGC这里是之前持有的指针由于GC后失效了,需要通过读屏障修正指针。❞
后面3行代码都不需要加读屏障:Object p = o这行代码并没有从堆中读取数据;o.doSomething()也没有从堆中读取数据;obj.fieldB不是对象引用,而是原子类型。
正是因为Load Barriers的存在,所以会导致配置ZGC的应用的吞吐量会变低。官方的测试数据是需要多出额外4%的开销: 那么,判断对象是Bad Color还是Good Color的依据是什么呢?就是根据上一段提到的Colored Pointers的4个颜色位。当加上读屏障时,根据对象指针中这4位的信息,就能知道当前对象是Bad/Good Color了。
PS:既然低42位指针可以支持4T内存,那么能否通过预约更多位给对象地址来达到支持更大内存的目的呢?答案肯定是不可以。因为目前主板地址总线最宽只有48bit,4位是颜色位,就只剩44位了,所以受限于目前的硬件,ZGC最大只能支持16T的内存,JDK13就把最大支持堆内存从4T扩大到了16T。
涉及对象:并发转移但还没做对象重定位的对象(着色指针使用M0和M1可以区分) 触发时机:在两次GC之间业务线程访问这样的对象 触发操作:对象重定位+删除转发表记录(两个一起做原子操作) 读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。 需要注意的是,仅"从堆中读取对象引用"才会触发这段代码。
3、运作过程概述
ZGC的运作过程大致可划分为以下四个大的阶段: 并发标记(Concurrent Mark)
:与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新颜色指针(见下面详解)中的Marked 0、 Marked 1标志位。
并发预备重分配(Concurrent Prepare for Relocate)
:这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
并发重分配(Concurrent Relocate)
:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table)
,记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障(见下面详解))所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的"自愈"(Self-Healing)能力。
ZGC的颜色指针因为"自愈"(Self-Healing)能力,所以只有第一次访问旧对象会变慢, 一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉, 因为可能还有访问在使用这个转发表。
并发重映射(Concurrent Remap)
:重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在"自愈"功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
4、ZGC详细流程
(1)一次ZGC流程
标记阶段(标识垃圾) 转移阶段(对象复制或移动)
(2)根可达算法
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
作为GC Roots的对象主要包括下面4种 虚拟机栈(栈帧中的本地变量表):各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。 方法区中类静态变量:java类的引用类型静态变量。 方法区中常量:比如:字符串常量池里的引用。 本地方法栈中JNI指针:(即一般说的Native方法)。
(3)ZGC中初始标记和并发标记
初始标记:从根集合(GC Roots)出发,找出根集合直接引用的活跃对象(根对象) 并发标记:根据初始标记找到的根对象,使用深度优先遍历对象的成员变量进行标记
(4)ZGC基于指针着色的并发标记算法
0.初始阶段 在ZGC初始化之后,此时地址视图为Remapped,程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动。
1、初始标记 这个阶段需要暂停(STW),初始标记只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,停顿时间不会随着堆的大小或者活跃对象的大小而增加。
2、并发标记 这个阶段不需要暂停(没有STW),扫描剩余的所有对象,这个处理时间比较长,所以走并发,业务线程与GC线程同时运行。但是这个阶段会产生漏标问题。
3、再标记 这个阶段需要暂停(没有STW),主要处理漏标对象,通过SATB算法解决(G1中的解决漏标的方案)。
(5)ZGC基于指针着色的并发转移算法
ZGC的转移阶段 并发转移准备(分析最有价值GC分页<无STW > ) 初始转移(转移初始标记的存活对象同时做对象重定位<有STW> ) 并发转移(对转移并发标记的存活对象做转移<无STW>)
如何做到并发转移? 转发表(类似于HashMap) 对象转移和插转发表做原子操作
(6)ZGC基于指针着色的重定位算法
并发标记对象的重定位 下次GC中的并发标记(同时做上次并发标记对象的重定位) 技术上:指针着色中M0和M1区分
5、ZGC中GC触发机制(JAVA16)
预热规则 :服务刚启动时出现,一般不需要关注。日志中关键字是"Warmup"。 JVM启动预热,如果从来没有发生过GC,则在堆内存使用超过10%、20%、30%时,分别触发一次GC,以收集GC数据. 基于分配速率的自适应算法 :最主要的GC触发方式(默认方式),其算法原理可简单描述为"ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC"。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。日志中关键字是"Allocation Rate"。 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。
主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。
阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是"Allocation Stall"。
外部触发:代码中显式调用System.gc()触发。 日志中关键字是"System.gc()"。
元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是"Metadata GC Threshold"。
6、ZGC参数设置
ZGC 优势不仅在于其超低的 STW 停顿,也在于其参数的简单,绝大部分生产场景都可以自适应。当然,极端情况下,还是有可能需要对 ZGC 个别参数做个调整,大致可以分为三类: 堆大小:Xmx
。当分配速率过高,超过回收速率,造成堆内存不够时,会触发 Allocation Stall,这类 Stall 会减缓当前的用户线程。因此,当我们在 GC 日志中看到 Allocation Stall,通常可以认为堆空间偏小或者 concurrent gc threads 数偏小。 GC 触发时机
:ZAllocationSpikeTolerance, ZCollectionInterval。ZAllocationSpikeTolerance 用来估算当前的堆内存分配速率,在当前剩余的堆内存下,ZAllocationSpikeTolerance 越大,估算的达到 OOM 的时间越快,ZGC 就会更早地进行触发 GC。ZCollectionInterval 用来指定 GC 发生的间隔,以秒为单位触发 GC。 GC 线程
:ParallelGCThreads, ConcGCThreads。ParallelGCThreads 是设置 STW 任务的 GC 线程数目,默认为 CPU 个数的 60%;ConcGCThreads 是并发阶段 GC 线程的数目,默认为 CPU 个数的 12.5%。增加 GC 线程数目,可以加快 GC 完成任务,减少各个阶段的时间,但也会增加 CPU 的抢占开销,可根据生产情况调整。
由上可以看出 ZGC 需要调整的参数十分简单,通常设置 Xmx 即可满足业务的需求,大大减轻 Java 开发者的负担。
启用ZGC比较简单,设置JVM参数即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」
。调优也并不难,因为ZGC调优参数并不多,远不像CMS那么复杂。它和G1一样,可以调优的参数都比较少,大部分工作JVM能很好的自动完成。下图所示是ZGC可以调优的参数:
四、ZGC注意事项
1、ZGC存在的问题
(1)浮动垃圾
ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些"朝生夕死"的对象没能及时的被回收。
目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。
2、注意事项
(1)RSS 内存异常现象
由前面 ZGC 原理可知,ZGC 采用多映射 multi-mapping 的方法实现了三份虚拟内存指向同一份物理内存。而 Linux 统计进程 RSS 内存占用的算法是比较脆弱的,这种多映射的方式并没有考虑完整,因此根据当前 Linux 采用大页和小页时,其统计的开启 ZGC 的 Java 进程的内存表现是不同的。在内核使用小页的 Linux 版本上,这种三映射的同一块物理内存会被 linux 的 RSS 占用算法统计 3 次,因此通常可以看到使用 ZGC 的 Java 进程的 RSS 内存膨胀了三倍左右,但是实际占用只有统计数据的三分之一,会对运维或者其他业务造成一定的困扰。而在内核使用大页的 Linux 版本上,这部分三映射的物理内存则会统计到 hugetlbfs inode 上,而不是当前 Java 进程上。
(2)共享内存调整
ZGC 需要在 share memory 中建立一个内存文件来作为实际物理内存占用,因此当要使用的 Java 的堆大小大于 /dev/shm 的大小时,需要对 /dev/shm 的大小进行调整。通常来说,命令如下(下面是将 /dev/shm 调整为 64G):
vi/etc/fstabtmpfs /dev/shm tmpfs defaults,size= 65536M00
首先修改 fstab 中 shm 配置的大小,size 的值根据需求进行修改,然后再进行 shm 的 mount 和 umount。
umount/dev/shmmount /dev/shm
(3)mmap 节点上限调整
ZGC 的堆申请和传统的 GC 有所不同,需要占用的 memory mapping 数目更多,即每个 ZPage 需要 mmap 映射三次,这样系统中仅 Java Heap 所占用的 mmap 个数为 (Xmx / zpage_size) * 3,默认情况下 zpage_size 的大小为 2M。
为了给 JNI 等 native 模块中的 mmap 映射数目留出空间,内存映射的数目应该调整为 (Xmx / zpage_size) 3*1.2。
默认的系统 memory mapping 数目由文件 /proc/sys/vm/max_map_count 指定,通常数目为 65536,当给 JVM 配置一个很大的堆时,需要调整该文件的配置,使得其大于 (Xmx / zpage_size) 3*1.2。
3、ZGC典型应用场景
对于性能来说,不同的配置对性能的影响是不同的,如充足的内存下即大堆场景,ZGC 在各类 Benchmark 中能够超过 G1 大约 5% 到 20%,而在小堆情况下,则要低于 G1 大约 10%;不同的配置对于应用的影响不尽相同,开发者需要根据使用场景来合理判断。
当前 ZGC 不支持压缩指针和分代 GC,其内存占用相对于 G1 来说要稍大,在小堆情况下较为明显,而在大堆情况下,这些多占用的内存则显得不那么突出。因此,以下两类应用强烈建议使用 ZGC 来提升业务体验:
1、超大堆应用
。超大堆(百 G 以上)下,CMS 或者 G1 如果发生 Full GC,停顿会在分钟级别,可能会造成业务的终端,强烈推荐使用 ZGC。 2、当业务应用需要提供高服务级别协议
(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此类应用无论堆大小,均推荐采用低停顿的 ZGC。
4、GC收集器选用
优先调整堆的大小让服务器自己来选择 如果内存小于100M,使用串行收集器 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择 如果允许停顿时间超过1秒,选择并行或者JVM自己选 如果响应时间最重要,并且不能超过1秒,使用并发收集器 4G-6G以下可以用parallel,6G-8G以上可以用G1,几百G以上用ZGC
参考资料
wiki.openjdk.org/display/zgc... cr.openjdk.org/~pliden/sli...