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,这个参数一般也是不需要动的。如果系统使用的软引用比较多,如反射用的非常多,可考虑关闭该参数。

相关推荐
斗-匕5 小时前
JVM 详解
jvm
Allen Bright9 小时前
【JVM-7】JVM 命令行工具 jstack 的使用和具体应用案例
java·开发语言·jvm
东北赵四9 小时前
JVM之垃圾回收器ZGC概述以及垃圾回收器总结的详细解析
java·jvm·算法
Allen Bright9 小时前
【JVM-5】深入解析JVM垃圾回收器:分类与原理
java·jvm
Allen Bright16 小时前
【JVM-3】深入理解JVM堆内存:结构、管理与优化
java·jvm
Allen Bright17 小时前
【JVM-4】深入解析JVM垃圾回收算法:原理、实现与优化
java·jvm·算法
东北赵四1 天前
JVM之垃圾回收器G1概述的详细解析
java·开发语言·jvm
东阳马生架构1 天前
G1原理—8.如何优化G1中的YGC
jvm
羽愿1 天前
JVM(双亲委派)
jvm