G1原理—9.如何优化G1中的MGC

大纲

1.大对象导致频繁Mixed GC的案例

2.Mixed GC到底是在优化什么(从避免到提速)

3.Mixed GC相关参数详解之堆内存分配参数

4.Mixed GC其他相关的参数详解及优化

1.大对象导致频繁Mixed GC的案例

(1)案例背景

(2)问题现场

(3)Redis缓存有什么问题

(4)缓存同步服务有什么问题

(5)线上环境的参数设置及调优

(1)案例背景

电商平台一般都有几种模式:淘宝这种属于长距离电商,买的东西可能来自于全国各地。京东属于中距离电商,有京东物流,通过最近的仓库发货 ,速度会快点。在此基础上还有近距离电商,如天猫超市、京东到家、京东小时购等。在这种近距离电商上面购买商品后,马上会有物流系统去接单进行配送。

有一个小时购电商系统:这个小时购电商系统的特点就是,需要使用大量的缓存。包括接入到平台的店铺信息及其商品信息、平台自营商品、店铺信息等。在商品中心里,商品更新、新商品上架这些操作会发送MQ。接着分批获取数据进行消费,更新Redis缓存,部分数据同步到本地缓存。

另外一个缓存更新的业务点是:有很多商品是批量上架的,尤其是自营商品,会直接批量上架一批商品。那么在商品更新后,会发送一条通知消息给这个缓存系统消费。缓存系统拿到通知消息后,去生成一个Job任务。然后执行这个Job,读取商品系统里面的一些数据,缓存到系统中。

(2)问题现场

整个过程,在平常系统正常运行时,是没什么问题的。因为商品本身上架的频率就不高,商品的一些数据更新也并不是很频繁。因此在日常运行中这个系统运行起来没有任何问题。从业务逻辑上来说,只要没特殊情况,这个业务逻辑也是没任何问题,因为只是一个缓存更新的逻辑而已。

然而在双十一前夕商户运营反馈:有少量商品的缓存信息不显示,或者显示很慢,实时性比较差。虽然这个问题听起来不是很严重,但是毕竟在大促节点是个问题。

于是开始去排查这个问题,具体的表现是:商品中心的QPS并不大,单台机器在高峰期只有500,总QPS不过5千。Redis集群也在一个比较健康的范围内,带宽、CPU、内存都比较合理。

那为什么还是会出现商品系统查询商品信息时加载慢的情况呢?查看商品系统的日志发现:在出现加载慢时,日志中有一条"写入数据到缓存中"的log日志。在这个日志打印后大概四五秒才继续往后打印日志。

那么这个日志代表的意思是什么呢?因为在缓存没有命中时,是需要从数据库中查询数据加载到缓存中去的。然后加载完成后,会把查询到的数据返回给前端。如果缓存设置卡住的时间比较长,就会造成请求的整体响应时间比较长。

在QPS不高的情况下出现查询超时,并且是经过数据库查询后,设置数据到缓存导致超时。而Redis本身又没什么问题,于是只能继续针对Redis本身来查问题。

(3)Redis缓存有什么问题

这里要补充一个细节:就是更新商品的缓存,是按照店铺来加锁的,因为这样做不至于在Redis中产生大量的分布式锁。其实按店铺的粒度已经够了,每个店铺更新商品的频率一般也不会很高。因为是小时购平台,所以更新商品要加锁,不能让用户购买更新前的商品。

在查Redis问题时,因为已经知道缓存更新时是按照店铺维度来加的锁,那么就直接查看一下这个锁相关的内容。结合Redis分布式锁的日志发现,获取锁失败进入等待的时间比较长。导致没有把缓存设置到Redis中,以至于这个请求的接口等待时间比较长。

知道这个问题后,开始尝试排查解决。首先要找到的就是为什么会超时?谁在持有这把锁,导致更新的时候出现的这么长时间的等待?要查这个问题,其实相对容易一点儿,直接通过Redis的客户端,来查看相关Redis分布式锁的lock key即可。Redis分布式锁的加锁本质就是先去获取一个key,然后在这个key对应的value里保存一些加锁的信息。

很快查到了匹配这个商铺相关的分布式锁,发现加锁的机器是小时购平台的缓存同步系统的其中一台机器。

到了这一步,很显然已经知道大概原因了。那就是因为这个缓存同步服务里的一些更新操作,获取到了锁,去更新这个商铺的商品缓存,然而更新的时间比较长,导致商品系统查询数据库 -> 更新缓存这个操作被阻塞了,最终导致接口请求时间非常长。

接下来就要找到这个原因,为什么同步缓存会这么久?

(4)缓存同步服务有什么问题

缓存同步服务持有锁这么长时间,导致其他的机器获取锁等待时间过久这个问题是什么原因?

排查GC日志后,发现当请求时间非常长的情况时,存在大量MGC,并且MGC持续时间长、频率非常高,基本上10分钟就要进行一轮MGC。每一轮MGC,基本上都要跑满8次,这就导致了超时的问题出现。

(5)线上环境的参数设置及调优

复制代码
 -XX:InitialHeapSize=20G -XX:MaxHeapSize=20G 
 -Xss1M -XX:+UseG1GC -XX:SurvivorRatio=8 
 -XX:MaxGCPauseMillis=300 -XX:G1HeapRegionSize=4M 
 -XX:MaxTenuringThreshold=15 -XX:InitiatingHeapOccupancyPercent=30

这就是当时线上环境的核心参数,目前已知因为Mixed GC过于频繁,导致了一些更新缓存的操作比较慢。

一.缓存系统定时对比补偿操作产生大量大对象

这里还有一个细节:就是Job任务不仅要批量更新缓存,还要进行缓存不一致的对比补偿操作。

在平常的系统运行,之所以没有出现问题。是因为Job定时更新缓存的频率不高,补偿job的频率和数据量也不高。总体没有造成影响。

而双十一期间,虽然看起来商品数据量没有变化太大。但是因为补偿任务和定时同步缓存的同时存在,导致有数倍于真实需要更新的商品数据的数据量(200w)要获取到系统中。并且补偿操作还是个定时执行的操作,把大量数据同步到缓存服务中。这就出现了大量的补偿操作带来大量的数据,导致内存资源被大量占用。

检查了JVM的GC日志,分析dump文件发现有大量的大对象,并且是缓存补偿操作下的Job持有的集合占用了大量的内存。继续观察GC日志发现,大对象分区占用的内存比例上升非常快。并且每次执行MGC时,都伴随着humongous allocation failer这种日志。

所以问题的原因就是产生了大量大对象,大量大对象进入了大对象分区。大对象分区占用的内存也会算在老年代占用里,从而导致频繁触发MGC。

二.大对象产生的原因是RegionSize在优化时被调小

之前的工程师对这个缓存服务系统的性能进行过优化。他想的是如果能让GC的时间更快,就能提升整体的性能。所以他认为太大的Region,可能会导致分析追踪对象时间过长。并且如果Region比较大,那么每个Region里存活对象的数量可能会太多,这就会导致选择性价比高的Region比较耗时。所以基于这些考虑,就把RegionSize由原来的16M调到了4M。

但实际上,这些过程造成的性能问题基本上是可以忽略的。这种操作属于因小失大,改成4M在平常数据量小的时候没什么问题。但如果数据量大时,即使分批次对比补偿,也是按几百的数据量去获取。

一般系统都会限制每批次取最多200条数据。每一条商品的数据信息对象,大概能达到十多K。十多K乘以200,刚刚好差不多是达到4M的一半。这就成功让获取到的数据信息对象成为大对象,而大量对象会直接进入大对象分区。

因为补偿的操作,也需要花费一定的时间。数据量大的时候,源源不断的拉取数据。处理对比数据的速率却跟不上,导致大量对象存活在大对象分区。最终导致频繁发生MGC,阻塞了正常的缓存同步操作,最终造成商品系统的缓存写入操作拿不到锁而阻塞。

三.如何解决缓存定时对比补偿操作中产生的大量大对象

第一步:就是把Region调大回16M,因为这是经过大量的压测调试最终确定的。当然也有一个比较取巧的方法:参考优秀开源系统的一些参数设置。但要尽量匹配系统,比如RocketMQ的Broker参数设置Region是16M。那么结合RocketMQ特点和系统是否匹配,考虑是否能借鉴其参数设置。

第二步:修改代码,调低Job补偿的频率,同时把Job每次处理的数据量调小一点,比如每次处理的数据量由200改为100。

第三步:修改新生代初始比例,调整为25,默认是5,即初始占用5%的堆内存。上述这个系统出问题时有大量数据要处理,处理不过来造成数据积压。如果不调整新生代大小,系统启动前期还是会出现频繁MGC的情况,因为会有大量存活对象经过YGC后进入到老年代区域。所以调大新生代的初始比例,可让系统在启动初期就能有比较高的吞吐。

2.Mixed GC到底是在优化什么(从避免到提速)

(1)优化Mixed GC之避免策略

(2)优化Mixed GC之提速策略

(1)优化Mixed GC之避免策略

优化思路主要是避免过多发生GC,通过修改代码、调整参数来实现间接调整YGC和MGC的持续时间、发生频率。

例如:通过调整RegionSize + TLAB Size,避免大对象分配 + 堆分配。通过调整RegionSize + 新生代大小 + 代码处理速度,减少MGC频率。这两个场景都是通过避免的思路来进行优化的。

关于避免思路的优化核心是:

避免什么 + 如何避免

第一:要避免慢速分配

在分配对象时要避免慢速分配,与慢速分配有关的因素。RegionSize大小、TLAB大小、refill_waste大小、工作线程数量等。

第二:要避免慢回收

就是要尽可能避免Miexd GC,因为速度比较慢,与慢回收有关的因素。RegionSize大小、新生代比例、老年代占比阈值、停顿时间。

这两个避免,也是我们日常工作中需要关注的重点调优点,大多数的调优思路其实都是来源于此。

(2)优化Mixed GC之提速策略

除了避免策略,还可以对Mixed GC的过程进行提速,来达到优化效果。也就是从分配、标记、回收、回收过程中的各种操作来提升处理速度。

关于提速思路的优化核心是:

提什么速 + 怎么提速

第一:提升分配速度

这个和避免慢速分配有异曲同工之妙,主要就是和TLAB相关的参数。在调大TLAB、调整refill_waste后,基本可以避免对象分配进入慢速分配。但是在此基础上,还要关注JVM之外的一些重要内容,比如工作线程的数量。

如果服务器是16核这种高性能机器,可以开几百个线程其实问题不大。如果服务器是4核这种普通服务器,就不能开几百个线程处理请求了。这样即使TLAB相关的参数调整得再好,分配的效率也还是会很低。所以提速主要就是根据服务器来确定工作线程的数量。

第二:提升回收的速度

比如在YGC阶段:参与回收的线程数量、DCQ相关白绿黄红四个区域设定、DCQ的长度设置、PLAB的大小设置都是一些提升回收速度的手段。

在服务器可承受的情况下,提升参与的线程数,肯定能提升回收效率。白绿黄红区域的阈值设置合理也能在一定的程度上减少GC过程中的处理压力,从而减少GC总时间。

PLAB缓冲区(对象复制时的缓存)如果足够充裕,也能够保证更快的回收。当然也可能会造成比较多的内存碎片,这个点依然是需要做很多权衡。

在Mixed GC阶段:SATB队列的长度GCDrainStackTargetSize、并发标记阶段处理时一次标记的最多对象个数、GC一次最多选择多少个分区进行回收、剩余多少垃圾时停止MixedGC回收等参数都可提速。

3.Mixed GC相关参数详解之堆内存分配参数

(1)-XX:InitiatingHeapOccupancyPercent的默认值为45

(2)-XX:G1ReservePercent默认为10

(3)-XX:G1HeapWastePercent默认为5%

Mixed GC参数介绍------堆内存分配相关内容:

复制代码
-XX:InitiatingHeapOccupancyPercent默认值为45,也就是45%
-XX:G1ReservePercent默认值为10,也就是10%
-XX:G1HeapWastePercent默认值为5%

(1)-XX:InitiatingHeapOccupancyPercent的默认值为45

当老年代内存占用总空间达到45%后,才会启动并发标记的任务。大对象分区也认为是属于老年代。这个值的大小调整需要经过反复的测试观察,才能调整到最优的比例。

一.如果这个值过小可能会频繁MGC,比如30%

那么在一次MGC后,老年代的占比可能又快速达到30%,从而频繁MGC。

二.如果这个值过大可能会频繁YGC,比如70%

虽然能避免频繁发生MGC,但又导致年轻代(如只占30%)又会频繁YGC。频繁YGC可能又会导致新生代的对象频繁触发晋升,从而进入老年代。大量的垃圾对象可能会占满老年代,如果发生晋升失败就会导致FGC。注:在出现晋升失败时也会导致FGC。

三.如果这个值设置得比较小,怎么保证尽量少发生FGC

假如这个值设置得比较小20%,那么老年代使用比例很快就会到达这个值,MGC触发的频率就会相对高很多。

只要系统是正常的,没有大量存活对象在老年代、造成空间不够用的问题,那么垃圾对象在晋升失败造成的FGC就基本可以避免了。

20%意味着新生代的比例会比较高,晋升到老年代的对象可能就不会太多。因为新生代内存足够,大多数请求都能在正常处理完时避免碰上YGC。

这个参数不太好设置,所以如果要去调节这个参数,可以综合来考虑。目的就是保证YGC、混合GC都比较快,同时Full GC比较少。

这里可以分享一个经验:如果要设置这个值,可以根据系统运行过程中的"平均使用内存"来设置。把这个值设置的和"平均使用内存"保持一致,这样整体的效率会高一些。

想要观察内存使用情况的话,可以打开打印Region详情的实验参数。

参数一:G1PrintHeapRegions

参数二:G1PrintRegionLivenessInfo

-XX:InitiatingHeapOccupancyPercent这个值设置好能极大提升性能,但如果想要设置得合理,就需要不断地尝试。

(2)-XX:G1ReservePercent默认为10

这个参数的含义是:在JVM初始化时,保留一部分分区不使用。这保留的部分分区,在新生代晋升老年代时,给晋升的对象来使用。

默认是将10%的空间留给新生代对象来处理晋升,如果因为新生代对象晋升失败导致的FGC比较多,那么可以适当调大这个值。

这个值的大小,也是和YGC、MGC息息相关的。如果这个值过大,则程序正常运行过程中实际可使用的空间就比较少,那么新生代、老年代可以用的空间就会比较少。如果这个值过大,还可能会在晋升过程中,造成晋升失败。

因为这个值过大,那么YGC就会比较少,从而导致晋升就会比较频繁。晋升频繁就可能导致预留空间快速填满,当预留区域不够就会导致FGC。

因此这个值一般不做改动,除非发现因为晋升失败导致的FGC比较频繁。比如系统一共发生了4次FGC,其中三次产生的原因是发生了晋升失败,那么此时就可以调大到15%。

(3)-XX:G1HeapWastePercent默认为5%

这个参数的含义是:如果开启了并发标记,标记结束后,根据CSet统计出垃圾对象的占比。如果垃圾对象占用整个堆内存的比例达到5%,就会开启多批次的MGC。如果比例达不到5%就不再进行MGC,等下一次YGC再次进行并发标记。

这个值可以决定MGC是否需要执行,也就是可以控制MGC的频率。如果MGC频率较高,同时每次回收的垃圾数量不多,就可以提高该值。

4.Mixed GC其他相关的参数详解及优化

(1)ParallelGCThreads

(2)ConcGCThreads

(3)HeapSizePerGCThread

(4)UseDynamicNumberOfGCThreads

(5)G1SATBBufferSize

(6)G1OldCSetRegionThresholdPercent

(7)GCDrainStackTargetSize

(8)ForceDynamicNumberOfGCThreads

(9)MarkStackSize和MarkStackSizeMax

(10)G1MixedGCLiveThreshoudPercent

(11)G1ConcMarkStepDurationMillis

(12)G1UseConcMarkReferenceProcessing

(1)ParallelGCThreads

这个参数是指定并行GC线程的数量,一般最好和CPU核心数量相当。如果不设置的话,会自动推断,计算公式是:

复制代码
当CPU数量小于8, ParallelGCThreads的值等于CPU数量;
当CPU数量大于8,则用公式:ParallelGCThreads = 8 + ((N - 8) * 5 / 8);

注意:这个值的意思是,并行执行GC操作时会有多少个线程参与。这个并行执行阶段,可以理解为处于STW过程中的阶段。这个阶段系统程序会完全停止,多个线程会并行处理标记、清理等任务。

(2)ConcGCThreads默认值为0

这个参数的含义是GC并发过程中的线程数量。如果没有设置的话,这个值会自动调整。调整的依据是:(ParallelGCThreads + 2) / 4。最小值为1,也就是至少为1个GC线程。

假如服务器是4核的,那计算出来得到的值就是:(4 + 2) / 4 = 1,也就是一个线程在工作,那么就是只有一个线程在并发过程中起作用。

如果发现并发过程速度比较慢,可以考虑增大该值。对于这个值,也要慎重调节。因为这个值如果调的过大,会导致系统程序的吞吐量下降。

并发线程数是指:这个并发执行阶段,系统是可以执行的,也就是GC过程中的非STW阶段。如果这个线程的数量过多,在程序和并发线程共同工作时:系统可以使用的CPU资源或者线程资源就越少,就会导致系统吞吐量下降。

(3)HeapSizePerGCThread默认值为64

这个参数的含义是,在GC过程中,每个线程处理的空间大小。可以简单的理解为:每64M的空间就分配一个线程去处理。

关于这个参数主要是要看服务器的计算能力,这个计算能力就不单单要考虑多少核了,而是处理器本身的能力是否强大。I3、I5、I7处理器假如都是4核,那肯定是I7处理器的计算性能更好。

不过因为使用的服务器多数是不需要考虑核心的计算性能的,所以一般这个参数的值就保持默认值即可,不需要做特别的调整。

(4)UseDynamicNumberOfGCThreads

这个参数的含义是,默认不能动态调整线程数量,默认为false。如果开启,则会按照最大线程数、HeapSizePerGCThread来动态调整。正常来说关闭即可,因为开启这个参数带来的收益非常有限。

普通的4核、16核机器还是目前服务器的主流,即使开启了这个参数其实也没有多大意义。反而有可能因为动态调整的判定逻辑,导致JVM需要额外消耗一部分性能去动态调整这个值。

(5)G1SATBBufferSize

表示每个SATB队列最多存放1000个灰色对象,这个参数正常来说也不需要修改,因为对整体性能没什么影响,默认值为1K。

(6)G1OldCSetRegionThresholdPercent

这个参数表示,每轮MGC回收的Region最大比例,默认值是Java堆的10%。如果执行了MGC回收,总共8轮,那么每轮不能回收超过堆内存10%的Region数量。假如停顿时间设置得比较短,每次的值可能会远远低于这个10%。这个参数一般也不需要做调整,保持默认即可。

(7)GCDrainStackTargetSize

这个参数表示并发标记阶段,一次最多能标记多少个对象,默认值为64。出于性能考虑,这个值不宜过大,也不宜过小。和HeapSizePerGCThread参数类似,也要基于处理器的运算能力。运算能力强可以适当提高这个参数的值,从而在一定程度提升并发标记的效率。

(8)ForceDynamicNumberOfGCThreads

这个参数是强制开启动态调整线程数,默认值为false。

(9)MarkStackSize和MarkStackSizeMax

这个参数表示:在并发标记阶段中用到的标记栈的大小。默认情况下,在32位JVM中为32K和4M,64位JVM中为4M和512M。

当然,如果没有设置,G1是会按照自动推断的方式计算设置这些参数,这个参数的调优结果在测试中没有显示出很好的效果。前后同样的环境,对系统影响不大。

在某种特殊场景下,可以调整此参数来尝试提升效率:当发现并发标记时间久,且并发标记的根对象数量和对象字段数量很大,此时就可以尝试调整此参数去提高并发标记的速度,避免标记栈过小。

注意:标记栈是有可能会溢出的。在溢出时,JVM会尝试停止标记操作。然后尝试扩展这个标记栈,这个过程是会降低标记效率的。

如果使用的是CMS垃圾回收器,也会有类似的参数:-XX:CMSMarkStackSize=8M,-XX:CMSMarkStackSizeMax=32M。这两个参数主要的意义就是,并发标记阶段,标记栈到达给多大。

对于G1来说:可认为在标记存活对象过程中,会把对象的一个个字段加入到一个栈中。这个栈就是由MarkStackSize这个参数所设置的,一般情况也不需要去设置,保持默认值即可。

这些冷门参数,一般情况下是不调整的。大多数情况下,只需要调整那些主流的参数其实满足大多数系统需求了。

(10)G1MixedGCLiveThreshoudPercent

这个参数是用于判断回收时选择Region加入CSet的阈值,默认值是85。如果符合条件的Region中的存活对象的比例小于85%,那么就可以加入CSet。其实就是判定Region是否有比较高的回收价值。

这个参数其实一般情况下也不需要调整,即使要去调整,也要有大量的测试数据来支撑。

如果这个参数调整得太大,比如95。那么只要存活对象比例小于95%的Region都会加入到CSet中去。可能会出现本来MGC进行3批次就可以完成回收,现在要8次才完成回收。带来的收益是:在回收时可能会有更多的对象被回收掉。带来的负面是:严重降低回收效率,因为95%的存活比例是非常高的。回收一个Region的时间 / 回收垃圾数量的比例会降低,整体效率比较低。

如果这个参数调整得太小,如小于50%的Region才会进入到CSet中回收,那么可能会导致一次MGC可以回收的垃圾对象数量就比较少。同时因大量50%以上使用率的Region并没有被回收,导致很多内存碎片。比较多内存碎片,导致内存利用效率比较低,可能会导致比较频繁的GC。

因此对于这个参数的调整还是需要综合考虑具体情况来具体分析,一般情况下,保持默认值不变即可。

(11)G1ConcMarkStepDurationMillis

这个参数的默认值为10,表示每次并发标记阶段执行的时间要在10ms内完成,并发标记其实是一个限时操作。所以MGC时,有可能会出现再次重新标记,有可能会出现多次并发标记。

这个值的大小可以影响到并发标记的频率。对于G1的混合回收,并发标记并不属于一次完整的混合回收。有可能出现多次并发标记才能完成所有的标记任务的。

那么这个参数的大小其实就可以决定并发标记的频率。如果时间调整的比较小,那么就意味着并发标记的频率会高一些。如果时间调整的比较大,那么并发标记的频率就会低一些。

如何调优?什么时候调整这个参数?如果要调优这个参数,需要知道并发标记的频率会带来什么影响。如果并发标记频率比较低,并发标记单次的时间比较长,会造成什么影响?系统是会不断运行的,新对象会不断产生,重新标记的时间也会比较长。

假如把并发标记的时间设为10s,那么是不是10s内就有可能触发FGC?因为垃圾对象太多,而MGC又卡在这个并发标记、重新标记这两步里。

所以默认值10ms一般是足够用了。如果FGC比较频繁时,除了空间分配合理,还有一个可能就是:并发标记不够及时,MGC不够及时,并发标记占用过多时间,回收不及时。

所以如果发现了频繁的FGC,通过各种比例的调整效果不好时:可以尝试减小这个并发标记时间,提高并发标记频率。从而减少重新标记时间,尽早完成混合回收。

(12)G1UseConcMarkReferenceProcessing

这个参数的意思是可以在并发标记时处理软引用相关的对象,默认值为true,这个参数一般也是不需要动的。如果系统使用的软引用比较多,如反射用的非常多,可考虑关闭该参数。

相关推荐
学到头秃的suhian14 小时前
JVM-类加载机制
java·jvm
NEFU AB-IN21 小时前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海1 天前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗1 天前
JVM整理
jvm
echoyu.1 天前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考2 天前
JVM中内存管理的策略
java·jvm
thginWalker2 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z2 天前
【JVM】详解 线程与协程
java·jvm
thginWalker2 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm