JVM GC总结

CMS垃圾回收器

参考:垃圾回收之CMS、G1、ZGC对比:https://developer.baidu.com/article/details/2770126
CMS(Concurrent Mark Sweep)垃圾回收器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,是HotSpot虚拟机第一款真正意义上的并发收集器它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
CMS垃圾回收器的优点是并发收集和停顿时间短。它在垃圾回收过程中几乎不会影响应用程序的运行,因此适用于对实时性能要求较高的应用场景

CMS的缺点:CMS收集器使用“标记-清除”算法实现。会产生内存碎片,可能会导致空间利用率降低。

  • 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
  • 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。
  • 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
  • 并发重置:重置本次GC过程中的标记数据。

参考:https://www.cnblogs.com/ladeng19/p/17659215.html 


G1垃圾回收器

参考:https://www.cnblogs.com/ladeng19/p/17659758.html

https://www.cnblogs.com/gdvxfgv/p/16037277.html
G1(Garbage-First)

优点是可预测的停顿时间和高吞吐量。

G1收集器将堆内存划分为多个独立的区域,并使用两个优先级队列来管理这些区域。优先级高的区域会被优先处理,从而保证在有限的停顿时间内完成垃圾回收任务。
它通过优先处理存活对象较少的区域来提高垃圾回收效率。

(但是G1可以设定一个参数(可以用JVM参数 -XX:MaxGCPauseMillis指定),这个参数可以设定gc的停顿时间,它会根据这个停顿时间,来判断垃圾回收到什么程度。即G1不一定会把垃圾全部回收完,当它回收到一定程度,停顿时间到了,他就会停止回收,故它可能只会回收80%的垃圾,就继续让客户线程运行了。)

G1还提供了更好的内存利用率和空间碎片控制。(基于复制
缺点:是可能会产生较大的停顿时间,特别是在处理优先级较低的区域时。
https://new.qq.com/rain/a/20240626A01C5U00


G1同时回收新生代和老年代,但是分别被称为G1的Young GC模式和Mixed GC模式。

这个特性来源于G1独特的内存布局,内存分配不再严格遵守新生代,老年代的划分,而是以Region为单位,G1跟踪各个Region的并且维护一个关于Region的优先级列表。在合适的时机选择合适的Region进行回收。这种基于Region的内存划分为一些巧妙的设计思想提供了解决停顿时间和高吞吐的基础。
region逻辑上划分为Eden,Survivor和老年代。每个分区都可能是eden区,Survivor区也可能是old区,但在一个时刻只能是一种分区;
Humongous用于存放大对象。当一个对象的容量超过了Region大小的一半,就会把这个对象放进Humongous分区,因为如果对一个短期存在的大对象使用复制算法回收的话,复制成本非常高。而直接放进old区则导致原本应该短期存在的对象占用了老年代的内存,更加不利于回收性能。

流程:

  • 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
  • 并发标记(Concurrent Marking):同CMS的并发标记
  • 最终标记(Remark,STW):同CMS的重新标记
  • 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)


复制
活着的对象拷贝到空的region,再回收掉部分region。这一步是采用多线程复制清除,整个过程会STW。这也是G1的优势之一,只要还有一块空闲的region,就可以完成垃圾回收。而不用像CMS那样必须预留太多的内存。


从G1的设计上来看,它使用了大量的额外结构来存储引用关系,并以此来减少垃圾回收中标记的耗时。但是这种额外的结构带来的内存浪费也是存在的,极端情况甚至可以额外占用20%的内存。而基于region的内存划分规则则让内存分配更加复杂,但是这也有好处。就是内存的回收后产生的碎片更少,也就更少触发full gc。
根据经验,在大部分的大型内存(6G以上)服务器上,无论是吞吐量还是STW时间,G1的性能都是要优于CMS。

三色标记算法(CMS & G1)


三色标记算法。以及三色标记算法的缺陷以及G1是如何解决这个缺陷的。
在可达性分析的思想指导下,我们需要标记对象是否可达,那么我们采用将对象标记为不同的颜色来区分对象是否可达。可以理解如果一个对象能从GC Roots出发并且遍历到,那么对象就是可达的,这个过程我们称为检查。
白色:对象还没被检查。
灰色:对象被检查了,但是对象的成员Field(对象中引用的其他对象)还没有被检查。这说明这个对象是可达的。
黑色:对象被检查了,对象的成员Fileld也被检查了。
漏标:
试想一下,在第二轮检查到第三轮检查之间,假设发生了引用的变化,objectD不再被objectB引用,而是被objectA引用,而且此时ObjectA的成员已经被检查完毕了,objectB的成员Field还没被检查。这时,objectD就永远不会再被检查到。这就导致了漏标。
还有一种漏标的情况,就是新产生一个对象,这个对象被已经被标记为黑色的对象持有
要解决漏标,要记录下发生更改的引用。所以我们需要一种记录引用发生更改的手段,写屏障(write barrier)。写屏障是一种记录下引用发生变更的手段,效果类似AOP

ZGC垃圾回收器


ZGC(Z Garbage Collector)是一款新一代的垃圾回收器,它在JDK 11中被引入。ZGC采用了读屏障(Read Barrier)和染色指针(Colored Pointer)等技术,实现了精确的内存管理,并能够在任意时间点暂停应用程序


优点:

提供了精确的内存管理和低停顿时间。

        它通过染色指针技术来追踪内存中的对象,避免了内存泄漏和程序崩溃等问题。此外,ZGC还支持大内存堆和高并发场景。

        ZGC只有三个STW阶段:初始标记再标记初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

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。

它的缺点是实现复杂度较高,可能会影响启动时间和内存占用。
综上所述,CMS、G1和ZGC三种垃圾回收器各有优缺点,适用于不同的应用场景。在实际开发中,我们应该根据应用的需求选择合适的垃圾回收器。如果需要高吞吐量和低停顿时间,可以选择G1或ZGC;如果需要并发收集和更好的空间利用率,可以选择CMS。

ZGC缺点细节:

OpenJDK11推荐使用G1而不是ZGC的4个原因:https://www.jianshu.com/p/8cb518d7fa6c
1)ZGC时Java进程占用三倍内存问题:由于ZGC着色指针把内存空间映射了3个虚拟地址,使得TOP/PS等命令查看占用内存时看到Java进程占用内存过大。此问题不影响操作系统,但是会影响到监控运维工具,需要注意。(实际自己开发监控的时候,就遇到了,从CMS&G1转ZGC,导致监控数据不一致)

2)同样的应用ZGC内存占用会多一些

主要原因是ZGC借用了对象头,导致无法使用指针压缩功能

4)吞吐量低于G1 GC。一般来说,可能会下降5%-15%。对于堆越小,这个效应越明显,堆非常大的时候,比如100G,其他GC可能一次Major或Full GC要几十秒以上,但是对于ZGC不需要那么大暂停。这种细粒度的优化带来的副作用就是,把很多环节其他GC里的STW整体处理,拆碎了,放到了更大时间范围内里去跟业务线程并发执行,甚至会直接让业务线程帮忙做一些GC的操作,从而降低了业务线程的处理能力。

总结:在OpenJDK11版本,建议优先使用G1 GC。如果想使用ZGC,建议使用OpenJDK17版本,或者使用KonaJDK 11/Dragonwell JDK 11。

ZGC:也是将堆内存划分为一系列的内存分区,称为page(深入理解JVM原书上管这种分区叫做region,但是官方文档还是叫做的page,我们这里引用官方文档的称呼以免和G1搞混),这种管理方式和G1 GC很相似
如果将ZGC设计为支持分代的垃圾回收,设计太复杂,所以先将ZGC设计为无分代的GC模式,可能后续再迭代。
ZGC的page有3种类型。
小型page(small page)
容量2M,存放小于256k的对象
中型page(medium page)
容量32M,存放大于等于256k,但是小于4M的对象
大型page(large page)
容量不固定,但是必须是2M的整数倍。存放4M以上的对象,且只能存放一个对象。

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很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

参考:https://www.cnblogs.com/ladeng19/p/17659758.html

新一代垃圾回收器ZGC的探索与实践 - 美团技术团队

初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html
ZGC只有三个STW阶段:初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

https://new.qq.com/rain/a/20240626A01C5U00
ZGC,也是目前号称"全程并发,能将停顿时间控制在10ms"内的低延迟垃圾回收器。ZGC全程Z Garbage Collector,这里面的Z不是什么缩写。

染色指针
Colored Pointer,染色指针是一种让指针存储额外信息的技术。
对于JVM来说,一个对象的地址只使用前42位,而第43-46位用来存储额外的信息,即GC对象处于ZGC那个阶段。只使用46位的客观原因是linux系统只支持46位的物理地址空间,即64T的内存。
在引用地址的划分上,对象引用第43位表示marked0标记,44位marked1标记,45位remapped标记,46位finalizable标记。
读屏障
read barrier,就是JVM向应用代码插入一小段代码的技术,仅当线程从堆中读取对象引用时触发。
读屏障主要是用来改写每个地址的命名空间。这里还涉及到指针的自愈self healing。指针的自愈是指的当访问一个正处于重分配集中的对象时会被读屏障拦截,然后通过转发记录表forwarding table将访问转发到新复制的对象上,并且修正并更新该引用的值,使其直接指向新对象。也是因为这个能力,ZGC的STW时间大大缩短,其他的GC则需要修复所有指向本对象的指针后才能访问。
并发标记阶段发生引用更改的对象会被记录下来(触发读屏障就会记录)
并发预备重分配(Concurrent Prepare for Relocate)
Forwarding table & Relocation Set
这一步主要是为了之后的迁移做准备,这一步主要是处理软引用,弱引用,虚引用对象,以及重置page的Forwarding table,收集待回收的page信息到Relocation Set
Forwarding table是记录对象被迁移后的新旧引用的映射表。Relocation Set是存放记录需要回收的存活页集合。
这个阶段ZGC会扫描所有的page,将需要迁移的page信息存储到Relocation Set,这一点和G1很不一样,G1是只选择部分需要回收的Region。在记录page信息的同时还会初始化page的Forwarding table,记录下每个page里有哪些需要迁移的对象。这一步耗时很长,因为是全量扫描所有的page,但是因为是和用户线程并发运行的,所以并不会STW

并发迁移(Concurrent Relocate)
这个阶段需要遍历所有的page,将存活的对象复制到其他page,然后再forward table里记录对象的新老引用地址的对应关系。
page中的对象被迁移完毕后,page就会被回收,注意这里并不会回收掉forward table,否则新老对象的映射关系就丢失了。
这个阶段如果正好用户线程访问了被迁移后的对象,那么也会根据forward table修正这个对象被持有的引用,这也是"指针的自愈"。

并发重映射(Concurrent Remap)
所以ZGC采取的办法是将这个阶段推迟到下轮ZGC的并发标记去完成,这样就共用了遍历所有对象的过程。
而在下次ZGC开始之前,任何的线程访问被迁移后的对象的引用,则可以触发读屏障,并根据forward table自己识别出对象被迁移后的地址,自行完成"指针自愈"。

ZGC的优势很明显,几乎全程并发的回收过程带来了无与伦比的低暂停时间,这也是ZGC的设计思路。低暂停时间加上JAVA本身的支持高并发的特点,假以时日ZGC将来一定是能在服务器领域的展现它大杀器级别的威力。但是为了达到这个设计目标,ZGC其实也牺牲了一些东西,比如吞吐量。
大家要好好学,尤其是现在ZGC还不是特别流行时,面试时多吹一吹,说不定能唬住一般的面试官。

ZGC在JDK16+版本(特别是当前LTS 17),还是推荐可以用的,修复了各类问题。特别是当前ZGC是不分代的,可以预期在下一个LTS版本,引入了分代处理,ZGC可以把STW停顿降低到1ms以内。

调优:

亿级流量电商系统如何优化JVM参数设置(ParNew+CMS)

大型电商系统后端现在一般都是拆分为多个子系统部署的,比如,商品系统,库存系统,订单系统,促销系统,会员系统等等。

我们这里以比较核心的订单系统为例

对于8G内存,我们一般是分配4G内存给JVM,正常的JVM参数配置如下:

-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8

这样设置可能会由于动态对象年龄判断原则导致频繁full gc。

于是我们可以更新下JVM参数设置:加大新生代对内存大小

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 

 

这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题,其实很多优化无非就是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc。

对于对象年龄应该为多少才移动到老年代比较合适,本例中一次minor gc要间隔二三十秒,大多数对象一般在几秒内就会变为垃圾,完全可以将默认的15岁改小一点,比如改为5,那么意味着对象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了,如果对象这么长时间都没被回收,完全可以认为这些对象是会存活的比较长的对象,可以移动到老年代,而不是继续一直占用survivor区空间。

对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),这个一般可以结合你自己系统看下有没有什么大对象生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,这些对象一般就是你系统初始化分配的缓存对象,比如大的缓存List,Map之类的对象。

可以适当调整JVM参数如下:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M 

对于JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),如果内存较大(超过4个G,只是经验值),系统对停顿时间比较敏感,我们可以使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)

对于老年代CMS的参数如何设置我们可以思考下,首先我们想下当前这个系统有哪些对象可能会长期存活躲过5次以上minor gc最终进入老年代。

无非就是那些Spring容器里的Bean,线程池对象,一些初始化缓存数据对象等,这些加起来充其量也就几十MB。

还有就是某次minor gc完了之后还有超过一两百M的对象存活,那么就会直接进入老年代,比如突然某一秒瞬间要处理五六百单,那么每秒生成的对象可能有一百多M,再加上整个系统可能压力剧增,一个订单要好几秒才能处理完,下一秒可能又有很多订单过来。

我们可以估算下大概每隔五六分钟出现一次这样的情况,那么大概半小时到一小时之间就可能因为老年代满了触发一次Full GC,Full GC的触发条件还有我们之前说过的老年代空间分配担保机制,历次的minor gc挪动到老年代的对象大小肯定是非常小的,所以几乎不会在minor gc触发之前由于老年代空间分配担保失败而产生full gc,其实在半小时后发生full gc,这时候已经过了抢购的最高峰期,后续可能几小时才做一次FullGC。

对于碎片整理,因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理,或者两到三次之后再做一次也行。

综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值,如下所示:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 
-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBefore

ZGC调优案例

我们维护的服务名叫Zeus,它是美团的规则平台,常用于风控场景中的规则管理。规则运行是基于开源的表达式执行引擎Aviator。Aviator内部将每一条表达式转化成Java的一个类,通过调用该类的接口实现表达式逻辑。

Zeus服务内的规则数量超过万条,且每台机器每天的请求量几百万。这些客观条件导致Aviator生成的类和方法会产生很多的ClassLoader和CodeCache,这些在使用ZGC时都成为过GC的性能瓶颈。接下来介绍两类调优案例。

内存分配阻塞,系统停顿可达到秒级

案例一:秒杀活动中流量突增,出现性能毛刺

日志信息:对比出现性能毛刺时间点的GC日志和业务日志,发现JVM停顿了较长时间,且停顿时GC日志中有大量的“Allocation Stall”日志。

分析:这种案例多出现在“自适应算法”为主要GC触发机制的场景中。ZGC是一款并发的垃圾回收器,GC线程和应用线程同时活动,在GC过程中,还会产生新的对象。GC完成之前,新产生的对象将堆占满,那么应用线程可能因为申请内存失败而导致线程阻塞。当秒杀活动开始,大量请求打入系统,但自适应算法计算的GC触发间隔较长,导致GC触发不及时,引起了内存分配阻塞,导致停顿。

解决方法:

(1)开启”基于固定时间间隔“的GC触发机制:-XX:ZCollectionInterval。比如调整为5秒,甚至更短。
(2)增大修正系数-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在各个集群上达到了一个更好表现。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值