目录
一、基本概述
二、基本关键技术知识总结
(一)三色标记法(着色指针)
(二)读屏障
(三)多图映射
(四)简单场景说明ZGC并发
三、基本回收原理介绍
四、ZGC调优案例实践
(一)调优基础知识分析
理解ZGC重要配置参数
理解ZGC触发时机
理解ZGC日志
理解ZGC停顿原因
(二)调优案例分析
案例一:秒杀活动中流量突增,出现性能毛刺
案例二:压测时,流量逐渐增大到一定程度后,出现性能毛刺
案例三: 单次GC停顿时间30ms,与预期停顿10ms左右有较大差距
案例四:服务启动后,运行时间越长,单次GC时间越长,重启后恢复
(三)升级ZGC效果
延迟降低
吞吐下降
五、业务升级JDK11与应用ZGC注意事项
评估收益
评估成本
评估风险
备注:升级JDK解决组件兼容性
参考文献、书籍及链接
一、基本概述
ZGC 是一款低延迟、高吞吐的垃圾回收器,由 Oracle 公司开发。它适用于大型、多核、内存容量较大的应用程序。ZGC 的设计目标是在最大限度地减少停顿时间的同时,为大型内存提供可伸缩性,并为生产部署提供高吞吐量和稳定性。它的目标是以不到 10 毫秒的暂停时间来控制 100MB 到 4TB 的内存。此外,ZGC 还致力于避免全局 JVM 暂停,从而提高系统的可用性。简单来说,它的设计目标是在不超过 10 毫秒的暂停时间内,尽可能地回收大量的堆内存。
ZGC 主要有以下几个特点:
- 低延迟:ZGC 的主要目标是最小化 GC 暂停时间。因此,ZGC 使用了基于读屏障(Read Barrier)的堆栈式(Stack-Style)替换算法,以及基于标记颜色(Mark-Color)的压缩算法,从而避免了传统 GC 中的根扫描和整理等阶段,大幅减少了 GC 暂停时间。
- 高吞吐:虽然 ZGC 的主要目标是低延迟,但它的吞吐性能也很不错。在低延迟的基础上,ZGC 通过多线程并行处理垃圾回收任务,以及使用更大的堆空间和更高效的内存分配器等技术,提高了垃圾回收的效率和吞吐量。
- 大堆支持:ZGC 支持的最大堆内存大小为 16TB,这使得它可以处理非常大的内存数据,例如云计算、大数据等领域的应用。
- 透明性:ZGC 对应用程序是透明的,应用程序无需进行任何修改,即可使用 ZGC 进行垃圾回收。
- 并发性:ZGC 是一款并发的垃圾回收器,它可以在运行应用程序的同时,进行垃圾回收操作。这使得 ZGC 可以在多核 CPU 上充分发挥并行处理能力,提高垃圾回收的效率。
总之,ZGC 是一款非常优秀的垃圾回收器,它通过独特的算法和设计,实现了低延迟、高吞吐、大堆支持、透明性和并发性等优势。
二、基本关键技术知识总结
(一)三色标记法(着色指针)
ZGC并发标记中使用的三色标记法,是用来防止在标记过程中,因为并发的应用程序线程导致的漏标问题而设计的。
在并发的垃圾回收标记过程中,错标不会影响垃圾回收算法的正确性,只是导致部分无用的对象不能及时释放。而漏标则会导致活跃的对象被清除从而影响程序的正确性。ZGC算法将对象用白色、灰色和黑色区分。
- 白色表示还没有被垃圾回收器标记
- 灰色表示对象其自身已经被标识到了,单拥有的成员变量所引用的其他对象还没有被标识
- 黑色表示自身已经被标识且成员变量对其他对象的应用也被标识。
产生漏标的充要条件是:已经完成标识的黑色对象新增了对某个白色对象的引用 && 正在被标记的灰色对象删除了对这个白色对象的引用。
解决方法是:在并发标记阶段,应用程序线程所访问到的白色对象都标记为灰色
(二)读屏障
读屏障(与之对应的有写屏障即Write Barrier,之前的GC都是采用Write Barrier,这次ZGC采用了完全不同的方案),这个是ZGC一个非常重要的特性。
在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个Load Barriers。看下面的代码:
Object o = obj.FieldA // 从堆中读取引用,需要加入屏障
<Load barrier needed here>
Object p = o // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i = obj.FieldB //无需加入屏障,因为不是对象引用
第一行代码我们尝试读取堆中的一个对象引用obj.fieldA并赋给引用o(fieldA也是一个对象时才会加上读屏障)。如果这时候对象在GC时被移动了,接下来JVM就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针“修正”到原本的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW。
JVM是如何判断对象被移动过呢?就是利用上面提到的颜色指针,如果指针是Bad Color,那么程序还不能往下执行,需要「slow path」,修正指针;如果指针是Good Color,那么正常往下执行即可
(三)多图映射
ZGC将内存空间划分为多个区域:
- [0~4TB) 对应Java堆
- [4TB ~ 8TB) 称为M0地址空间
- [8TB ~ 12TB) 称为M1地址空间
- [12TB ~ 16TB) 预留未使用
- [16TB ~ 20TB) 称为Remapped空间。
当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。
ZGC支持64位系统,其中低42位(0~41位)用于描述真正的虚拟地址,接着4位(42~45位)用于描述元数据,46位保留,高17位固定为0。其中M0、M1和Remapped这三种视图分别是将42、43、44位置为1,在操作系统寻址时会将这3位和前42位结合,从而形成三套虚拟地址空间。
多视图映射是如何在ZGC并发算法中发挥作用的。
初始化阶段:此时地址视图为Remapped,触发gc后进入并发标记阶段
并发标记阶段:
gc线程:将活跃对象的视图从Remapped置为M0
应用程序线程:如果创建新对象,直接置为M0;如果访问到了Remapped对象,也置为M0
并发转移阶段:
gc线程:将M0视图对象转移并至为Remapped
应用程序线程:访问到M0对象则将其转移,并置为Remapped
(四)简单场景说明ZGC并发
应用程序创建对象1、2、3,此刻他们都是Remapped,触发gc后进入标记阶段,假设0和2是活跃对象被标记为M0,1为非活跃对象还保持Remapped。在该阶段新创建的对象3也是M0.
转移阶段,0和1所在页面被回收,因为0是活跃对象被标记为M0,因此在内存回收前辈转移到了新的页面,并置为Remapped。2和3没有被转移,继续保质M0视图。在该阶段新建的对象是Remapped视图。
新一轮的gc启动,标记活跃对象为M1,不活跃的对象保持Remapped,2和3没有被访问所以还是保持M0,该阶段新建的对象是M1视图。
三、基本回收原理介绍
ZGC相较于CMS和G1的重大改进点在于:基于多图映射、三色标记法、读屏障技术实现,在标记和转移对象的过程都支持与应用程序线程并发。
ZGC执行流程:
-
初始标记:从根集合出发,找到直接引用的活跃对象,该阶段需要STW(相较于G1的初始标记,仅仅找到根集合直接引用对象,耗时极大优化,预计1ms完成)
-
并发标记:以初始标记所找到的对象为根集合,使用深度优先遍历所有引用对象,使用三色标记法避免漏标活跃对象。
-
再标记和非强根并行标记:在并发标记结束后尝试终结标记动作,理论上并发标记结束后所有待标记的对象会全部完成,但是因为GC工作线程和应用程序线程是并发运行,所以可能存在GC工作线程执行结束标记时,应用程序线程又有新的引用关系变化导致漏标记,所以这一步先判断是否真的结束了对象的标记,如果没有结束就还会启动并行标记,所以这一步需要STW。另外,在该步中,还会对非强根进行并行标记。
-
并发处理非强引用和非强根并发标记:在非强引用处理时对定义了finalize()函数的对象需要特殊处理,为此ZGC设计了特殊的标记另外,ZGC为了优化停顿时间,把一些需要在STW中并行处理的任务并发运行,这都被设计成非强根的并发标记。
-
并发选择对象的转移集合:转移集合中就是待回收的页面。
-
并发初始化转移集合中的每个页面:在后续重定位(也称为Remap)时需要的对象转移表(Forward Table)就是在这一步初始化的。
-
转移根对象引用的对象:该步需要STW。
-
并发转移:把对象移动到新的页面中,这样对象所在的老的页面中所有活跃对象都被转移了,页面就可以被回收重用。该阶段使用读屏障来保证应用程序线程能够获取到迁移对象的最新地址,使用多图映射来保证迁移过程的并发。
四、ZGC调优案例实践
这部分内容主要来自【美团技术团队搬运】新一代垃圾回收器ZGC的探索与实践-蒲公英云
(一)调优基础知识分析
理解ZGC重要配置参数
-Xms10G -Xmx10G
-XX:ReservedCodeCacheSize=256m -XX:InitialCodeCacheSize=256m
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
-XX:ConcGCThreads=2 -XX:ParallelGCThreads=6
-XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m
- -Xms -Xmx: 堆的最大内存和最小内存,这里都设置为10G,程序的堆内存将保持10G不变。
- -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize: 设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m就已经足够。我们的服务因为有一定特殊性,所以设置的较大,后面会详细介绍。
- -XX:+UnlockExperimentalVMOptions -XX:+UseZGC: 启用ZGC的配置。
- -XX:ConcGCThreads: 并发回收垃圾的线程。默认是总核数的12.5%,8核CPU默认是1。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响。
- -XX:ParallelGCThreads: STW阶段使用线程数,默认是总核数的60%。
- -XX:ZCollectionInterval: ZGC发生的最小时间间隔,单位秒。
- -XX:ZAllocationSpikeTolerance: ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。
- -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive: 是否启用主动回收,默认开启,这里的配置表示关闭。
- -Xlog: 设置GC日志中的内容、格式、位置以及每个日志的大小。
理解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”。
理解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个小时内,从启动到现在的所有统计信息。利用这些统计信息,可以排查定位一些异常点。
日志中内容较多,关键点已用红线标出,含义较好理解,更详细的解释大家可以自行在网上查阅资料。
理解ZGC停顿原因
我们在实战过程中共发现了6种使程序停顿的场景,分别如下:
- GC时,初始标记:日志中Pause Mark Start。
- GC时,再标记:日志中Pause Mark End。
- GC时,初始转移:日志中Pause Relocate Start。
- 内存分配阻塞:当内存不足时线程会阻塞等待GC完成,关键字是"Allocation Stall"。
- 安全点:所有线程进入到安全点后才能进行GC,ZGC定期进入安全点判断是否需要GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。
- dump线程、内存:比如jstack、jmap命令。
(二)调优案例分析
维护服务名叫Zeus,是美团的规则平台,常用于风控场景中的规则管理。规则运行是基于开源的表达式执行引擎Aviator。Aviator内部将每一条表达式转化成Java的一个类,通过调用该类的接口实现表达式逻辑。
Zeus服务内的规则数量超过万条,且每台机器每天的请求量几百万。这些客观条件导致Aviator生成的类和方法会产生很多的ClassLoader和CodeCache,这些在使用ZGC时都成为过GC的性能瓶颈。接下来介绍两类调优案例。
内存分配阻塞,系统停顿可达到秒级
案例一:秒杀活动中流量突增,出现性能毛刺
日志信息:对比出现性能毛刺时间点的GC日志和业务日志,发现JVM停顿了较长时间,且停顿时GC日志中有大量的“Allocation Stall”日志。
分析:这种案例多出现在“自适应算法”为主要GC触发机制的场景中。ZGC是一款并发的垃圾回收器,GC线程和应用线程同时活动,在GC过程中,还会产生新的对象。GC完成之前,新产生的对象将堆占满,那么应用线程可能因为申请内存失败而导致线程阻塞。当秒杀活动开始,大量请求打入系统,但自适应算法计算的GC触发间隔较长,导致GC触发不及时,引起了内存分配阻塞,导致停顿。
解决方法:
- 开启”基于固定时间间隔“的GC触发机制:-XX:ZCollectionInterval。比如调整为5秒,甚至更短。
- 增大修正系数-XX:ZAllocationSpikeTolerance,更早触发GC。ZGC采用正态分布模型预测内存分配速率,模型修正系数ZAllocationSpikeTolerance默认值为2,值越大,越早的触发GC,Zeus中所有集群设置的是5。
案例二:压测时,流量逐渐增大到一定程度后,出现性能毛刺
日志信息:平均1秒GC一次,两次GC之间几乎没有间隔。
分析:GC触发及时,但内存标记和回收速度过慢,引起内存分配阻塞,导致停顿。
解决方法:增大-XX:ConcGCThreads, 加快并发标记和回收速度。ConcGCThreads默认值是核数的1/8,8核机器,默认值是1。注意,该参数影响系统吞吐,如果GC间隔时间大于GC周期,不建议调整该参数。
GC Roots 数量大,单次GC停顿时间长
案例三: 单次GC停顿时间30ms,与预期停顿10ms左右有较大差距
日志信息:观察ZGC日志信息统计,“Pause Roots ClassLoaderDataGraph”一项耗时较长。
分析:dump内存文件,发现系统中有上万个ClassLoader实例。我们知道ClassLoader属于GC Roots一部分,且ZGC停顿时间与GC Roots成正比,GC Roots数量越大,停顿时间越久。再进一步分析,ClassLoader的类名表明,这些ClassLoader均由Aviator组件生成。分析Aviator源码,发现Aviator对每一个表达式新生成类时,会创建一个ClassLoader,这导致了ClassLoader数量巨大的问题。在更高Aviator版本中,该问题已经被修复,即仅创建一个ClassLoader为所有表达式生成类。
解决方法:升级Aviator组件版本,避免生成多余的ClassLoader。
案例四:服务启动后,运行时间越长,单次GC时间越长,重启后恢复
日志信息:观察ZGC日志信息统计,“Pause Roots CodeCache”的耗时会随着服务运行时间逐渐增长。
分析:CodeCache空间用于存放Java热点代码的JIT编译结果,而CodeCache也属于GC Roots一部分。通过添加-XX:+PrintCodeCacheOnCompilation参数,打印CodeCache中的被优化的方法,发现大量的Aviator表达式代码。定位到根本原因,每个表达式都是一个类中一个方法。随着运行时间越长,执行次数增加,这些方法会被JIT优化编译进入到Code Cache中,导致CodeCache越来越大。
解决方法:JIT有一些参数配置可以调整JIT编译的条件,但对于我们的问题都不太适用。我们最终通过业务优化解决,删除不需要执行的Aviator表达式,从而避免了大量Aviator方法进入CodeCache中。
值得一提的是,我们并不是在所有这些问题都解决后才全量部署所有集群。即使开始有各种各样的毛刺,但计算后发现,有各种问题的ZGC也比之前的CMS对服务可用性影响小。所以从开始准备使用ZGC到全量部署,大概用了2周的时间。在之后的3个月时间里,我们边做业务需求,边跟进这些问题,最终逐个解决了上述问题,从而使ZGC在各个集群上达到了一个更好表现。
(三)升级ZGC效果
延迟降低
TP(Top Percentile)是一项衡量系统延迟的指标:TP999表示99.9%请求都能被响应的最小耗时;TP99表示99%请求都能被响应的最小耗时。
在Zeus服务不同集群中,ZGC在低延迟(TP999 < 200ms)场景中收益较大:
-
TP999:下降12~142ms,下降幅度18%~74%。
-
TP99:下降5~28ms,下降幅度10%~47%。
超低延迟(TP999 < 20ms)和高延迟(TP999 > 200ms)服务收益不大,原因是这些服务的响应时间瓶颈不是GC,而是外部依赖的性能。
吞吐下降
对吞吐量优先的场景,ZGC可能并不适合。例如,Zeus某离线集群原先使用CMS,升级ZGC后,系统吞吐量明显降低。究其原因有二:第一,ZGC是单代垃圾回收器,而CMS是分代垃圾回收器。单代垃圾回收器每次处理的对象更多,更耗费CPU资源;第二,ZGC使用读屏障,读屏障操作需耗费额外的计算资源。
五、业务升级JDK11与应用ZGC注意事项
在生产环境升级JDK11,使用ZGC,在使用新技术前,首先要做的是评估收益、成本和风险。
评估收益
对于JDK这种世界关注的程序,大版本升级所引入的新技术一般已经在理论上经过验证。我们要做的事情就是确定当前系统的瓶颈是否是新版本JDK可解决的问题,切忌问题未诊断清楚就采取措施。评估完收益之后再评估成本和风险,收益过大或者过小,其他两项影响权重就会小很多。
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停顿对响应时间的影响较大。
评估成本
这里主要指升级所需要的人力成本。此项相对比较成熟,根据新技术的使用手册判断改动点。跟做其他项目区别不大,不再具体细说。在实践中,两周时间完成线上部署,达到安全稳定运行的状态。后续持续迭代3个月,根据业务场景对ZGC进行了更契合的优化适配。
评估风险
升级JDK的风险可以分为三类:
- 兼容性风险:Java程序JAR包依赖很多,升级JDK版本后程序是否能运行起来。例如我们的服务是从JDK7升级到JDK11,需要解决较多JAR包不兼容的问题。
- 功能风险:运行起来后,是否会有一些组件逻辑变更,影响现有功能的逻辑。
- 性能风险:功能如果没有问题,性能是否稳定,能稳定的在线上运行。
经过分类后,每类风险的应对转化成了常见的测试问题,依旧存在风险,需排期(通过完备的单测、集成和回归测试,保证功能正确性)覆盖主链路所有场景并做线上灰度,灰度较长时间没问题后在进行上线。
备注:升级JDK解决组件兼容性
具体过程如下:
- 编译,需要修改pom文件中的build配置,根据报错作修改,主要有两类:一些类被删除:比如“sun.misc.BASE64Encoder”,找到替换类java.util.Base64即可。组件依赖版本不兼容JDK11问题:找到对应依赖组件,搜索最新版本,一般都支持JDK11。
- 编译成功后,启动运行,此时仍有可能组件依赖版本问题,按照编译时的方式处理即可。
<dependency><groupId>javax.annotation</groupId><artifactId>javax.annotation-api</artifactId><version>1.3.2</version>
</dependency>
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.4</version>
</dependency>
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator-parent</artifactId><version>6.0.16.Final</version>
</dependency>
<dependency><groupId>com.sankuai.inf</groupId><artifactId>patriot-sdk</artifactId><version>1.2.1</version>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version>
</dependency>
<dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version>
</dependency>
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.39.Final</version>
</dependency>
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version>
</dependency>
DK11已经出来两年,常见的依赖组件都有兼容性版本。但是,如果是公司内部提供的公司级组件,可能会不兼容JDK11,需要推动相关组件进行升级。如果对方升级较为困难,可以考虑拆分功能,将依赖这些组件的功能单独部署,继续使用低版本JDK。随着JDK11的卓越性能被大家悉知,相信会有更多团队会用JDK11解决GC问题,使用者越多,各个组件升级的动力也会越大。
参考文献、书籍及链接
1.【美团技术团队搬运】新一代垃圾回收器ZGC的探索与实践-蒲公英云
2.ZGC官网
3.彭成寒.《新一代垃圾回收器ZGC设计与实现》. 机械工业出版社, 2019.