-Xms4096M -Xmx4096M -Xmn2048M``-XX:MetaspaceSize=256M``-XX:MaxMetaspaceSize=256M``-XX:+UseParNewGC``-XX:+UseConcMarkSweepGC` `-XX:+CMSScavengeBeforeRemark
灰度3台机器。
我们先分析下Young GC相关指标:
Young GC次数
Young GC累计耗时
Young GC单次耗时
可以看出,与原始方案相比,目标方案的YGC次数减少50%,累积耗时减少47%,吞吐量提升的同时,服务停顿的频率大大降低,而代价是单次Young GC的耗时增长3ms,收益是非常高的。
对照方案2即Young区2G的方案整体表现稍逊与目标方案,再分析Full GC指标。
老年代内存增长情况
Full GC次数
Full GC累计/单次耗时
与原始方案相比,使用目标方案时,老年代增长的速度要缓慢很多,基本在观测周期内Full GC发生的次数从155次减少至27次,减少82%,停顿时间均值从399ms减少至60ms,减少85%,毛刺也非常少。
对照方案2即Young区2G的方案整体表现逊于目标方案。到这里,可以看出,目标方案从各个维度均远优于原始方案,调优目标也基本达成。
但细心的同学会发现,目标方案相对原始方案,"Full GC"(实际上是CMS Background GC)耗时更加平稳,但每个若干次"Full GC"后会有一个耗时很高的毛刺出现,这意味这个用户请求在这个时刻会停顿2-3s,能否进一步优化,给用户一个更加极致的体验呢?
这里首先要分析这现象背后的逻辑。
对于CMS搜集器,采用的搜集算法为Mark-Sweep-[Compact]。
CMS搜集器GC的种类:
CMS Background GC
这种GC是CMS最常见的一类,是周期性的,由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,采用的是Mark-Sweep方式,由于没有Compact这种耗时操作,且可以与用户进程并行,所以CMS的停顿会比较低,GC日志中出现GC (CMS Initial Mark)字样就代表发生了一次CMS Background GC。
Background GC由于采用的是Mark-Sweep,会导致老年代内存碎片,这也是CMS最大的弱点。
CMS Foreground GC
这种GC是CMS搜集器里真正意义上的Full GC,采用Serial Old或Parralel Old进行收集,出现的频率就较低,当往往出现后就会造成较大的停顿。
触发CMS Foreground GC的场景有很多,场景的如下:
System.gc();
jmap -histo:live pid;
元数据区域空间不足;
晋升失败,GC日志中的标志为ParNew(promotion failed);
并发模式失败,GC日志中的标志为councurrent mode failure字样。
不难推断,目标方案中的毛刺是晋升失败或并发模式失败造成的,由于线上没有开启打印gc日志,但也无妨,因为这两种场景的根因是一致的,就是若干次CMS Backgroud GC后造成的老年代内存碎片。
我们只需要尽可能减少由于老年代碎片触发晋升失败、并发模式失败即可。
CMS Background GC由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,该阈值由-XX:CMSInitiatingOccupancyFraction;
-XX:+UseCMSInitiatingOccupancyOnly两个参数控制,不设置,默认首次为92%,后续会根据历史情况进行预测,动态调整。
如果我们固定阈值的大小,将该阈值设置为一个相对合理的值,既不使GC过于频繁,又可以降低晋升失败或并发模式失败的概率,就可以大大缓解毛刺产生的频率。
目标方案的堆分布如下:
Young区 1.5G
Old区 2.5G
Old区常驻对象 约400M
按经验数据,75%,80%是比较折中的,因此我们选择-XX:CMSInitiatingOccupancyFraction=75 -
XX:+UseCMSInitiatingOccupancyOnly进行灰度观察(我们也对80%的场景做了对照实验,75%优于80%)。
最终目标方案的配置为:
-Xms4096M -Xmx4096M -Xmn1536M` `-XX:MetaspaceSize=256M` `-XX:MaxMetaspaceSize=256M` `-XX:+UseParNewGC` `-XX:+UseConcMarkSweepGC` `-XX:+CMSScavengeBeforeRemark` `-XX:CMSInitiatingOccupancyFraction=75` `-XX:+UseCMSInitiatingOccupancyOnly
如上配置,灰度 xx.xxx.60.6 一台机器;
从再次优化的结果上看,CMS Foreground GC引起的毛刺基本消失,符合预期。
因此,视频服务最终目标方案的配置为;
-Xms4096M -Xmx4096M -Xmn1536M` `-XX:MetaspaceSize=256M` `-XX:MaxMetaspaceSize=256M` `-XX:+UseParNewGC` `-XX:+UseConcMarkSweepGC` `-XX:+CMSScavengeBeforeRemark` `-XX:CMSInitiatingOccupancyFraction=75` `-XX:+UseCMSInitiatingOccupancyOnly
灰度持续7天左右,覆盖工作日与周末,结果符合预期,因此符合在线上开启全量的条件,下面对全量后的结果进行评估。
Young GC次数
Young GC累计耗时
单次Young GC耗时
从Young GC指标上看,调整后Young GC次数平均减少30%,Young GC累积耗时平均减少17%,Young GC单次耗时平均增加约7ms,Young GC的表现符合预期。
除了技术手段,我们也在业务上做了一些优化,调优前实例的Young GC会出现明显的、不规律的(定时任务不一定分配到当前实例)毛刺,这里是业务上的一个定时任务,会加载大量数据,调优过程中将该任务进行分片,分摊到多个实例上,进而使Young GC更加平滑。
Full GC单次/累积耗时
从"Full GC"的指标上看,"Full GC"的频率、停顿极大减少,可以说基本上没有真正意义上的Full GC了。
核心接口-A (下游依赖较多) P99响应时间,减少19%(从 3457 ms下降至 2817 ms);
核心接口-B (下游依赖中等) P99响应时间,减少41%(从 1647ms下降至 973ms);
核心接口-C (下游依赖最少) P99响应时间,减少80%(从 628ms下降至 127ms);
综合来看,整个结果是超出预期的。Young GC表现与设定的目标非常吻合,基本上没有真正意义上的Full GC,接口P99的优化效果取决于下游依赖的多少,依赖越少,效果越明显。
由于GC算法复杂,影响GC性能的参数众多,并且具体参数的设置又取决于服务的特点,这些因素都很大程度增加了JVM调优的难度。
本文结合视频服务的调优经验,着重介绍调优的思路和落地过程,同时总结出一些通用的调优流程,希望能给大家提供一些参考。
来源:url.cy/2B8j24
我的微信公众号:Java架构师进阶编程
专注分享Java技术干货,期待你的关注!