前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GC和垃圾回收器其三:JVM调优原则

GC和垃圾回收器其三:JVM调优原则

作者头像
春哥大魔王
发布2019-09-05 16:53:02
1.4K0
发布2019-09-05 16:53:02
举报

写在前面

谈到调优,首先要理解调优的目标是什么,针对什么样的场景,做什么样的事情。

性能角度主要瞄准三个方向:内存占用,时延,吞吐。在这些角度之外可能围绕OOM是否合理,GC参数是否合理,或者启动速度方面等。

  • 高可用:可用性几个9
  • 低延迟:请求必须多少时间内完成
  • 高吞吐:每秒完成多少次事务

作为网关系统低延迟和高可用是核心指标,针对这个目标进行优化。

假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25ms,GC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。

举个例子:

服务情况:Minor GC每分钟100次 ,Major GC每4分钟一次,单次Minor GC耗时25ms,单次Major GC耗时200ms,接口响应时间50ms。

由于这个服务要求低延时高可用,结合GC对服务响应时间的影响,计算可知由于Minor GC的发生,12.5%的请求响应时间会增加,其中8.3%的请求响应时间会增加25ms,可见当前GC情况对响应时间影响较大。

(50ms+25ms)× 100次/60000ms = 12.5%,50ms × 100次/60000ms = 8.3% 。

优化目标:降低TP99、TP90时间。

JVM启动参数有哪些不合理的

CMSInitiatingOccupancyFraction

本着小步快跑的原则,先从JVM启动参数入手看看有什么可以做的,首先映入眼帘的两个特别突出的参数-XX:CMSInitiatingOccupancyFraction=75 XX:+UseCMSInitiatingOccupancyOnly,意味着老年代(3G)占用75%(2.25G)触发fullgc,考虑到老年代巨大的碎片,老年代可能在占用1.9G时,担保失败就会被触发,导致2.8秒的长时间停顿。

MaxTenuringThreshold

MaxTenuringThreshold 决定了对象进入老年代的年龄,如果年龄过小导致new区进入old区对象过快,导致频繁old gc。调大MaxTenuringThreshold之后old区一直为空,但是ygc time则涨了很多,每次都要0.2s,比之前ygc time多了3倍,这是无法接受的。

CMSScavengeBeforeRemark

由于CMS在remark阶段是STW的,所以为减少对整个堆的扫描,可以配置CMSScavengeBeforeRemark 在remark之前进行一次ygc。

观察GC日志

各区大小对GC性能影响较大,将各区大小调整到合适大小,分析活跃数据大小是很好的切入点。

活跃数据指的是应用程序稳定运行时间长期存活对象在堆中占用的空间大小,就是full gc后堆中老年代占用空间大小。可以通过gc日志中full gc之后老年代数据大小得出。取多次gc之后数据大小的平均值。

例如,根据GC日志获得老年代的活跃数据大小为300MB,那么各分区大小可以设为:总堆:1200MB = 300MB × 4 新生代:450MB = 300MB × 1.5 老年代:750MB = 1200MB - 450MB*

这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。

调优

YGC

首先优化Minor GC频繁问题。通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率。在相同的内存分配率的前提下,新生代中的Eden区增加一倍,Minor GC的次数就会减少一半。

扩容Eden区虽然可以减少Minor GC的次数,但会增加单次Minor GC时间么?根据上面公式,如果单次Minor GC时间也增加,很难保证最后的优化效果。单次Minor GC时间主要受哪些因素影响?是否和新生代大小存在线性关系?

扩容后,Minor GC时增加了T1(扫描时间),但省去T2(复制对象)的时间,更重要的是对于虚拟机来说,复制对象的成本要远高于扫描成本,所以,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加。

服务中存在大量短期临时对象,扩容新生代空间后,Minor GC频率降低,对象在新生代得到充分回收,只有生命周期长的对象才进入老年代。这样老年代增速变慢,Major GC频率自然也会降低。

通过扩容新生代为为原来的三倍,单次Minor GC时间增加小于5ms,频率下降了60%,服务响应时间TP90,TP99都下降了10ms+,服务可用性得到提升。

Old GC

  • new threshold = 2(动态年龄判断,对象的晋升年龄阈值为2),对象仅经历2次Minor GC后就晋升到老年代,这样老年代会迅速被填满,直接导致了频繁的Major GC。
  • Major GC后老年代使用空间为300Mb+,意味着此时绝大多数(86% = 2G/2.3G)的对象已经不再存活,也就是说生命周期长的对象占比很小。

动态年龄计算:Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。调优前:Survivor区 = 64M,desired survivor = 32M,此时Survivor区中age<=2的对象累计大小为41M,41M大于32M,所以晋升年龄阈值被设置为2,下次Minor GC时将年龄超过2的对象被晋升到老年代。

最初考虑将老年代大小翻倍到6G,可以帮助减少碎片化问题,但是增加老年代只能延迟整体清理时间,并不能杜绝碎片问题和晋升担保问题,导致停顿达到了10.76秒。

CMS本身缺点之一就是碎片问题,随着长时间运行碎片问题可能越来越严重。

如果晋升对象大于连续内存空间,那么fullgc就被触发了,fullgc意味着STW,直到清理完成为止。堆越大停顿时间就越长,所以增大内存并不能解决问题,只能拖延长时间停顿,并看到更长的GC停顿。

终极疗法

总之CMS无法解决碎片问题,终极疗法升级到G1,G1的优势:

  1. CMS没有复制算法,无法压缩,导致碎片,采用G1使用复制算法,通过将对象在多个region之间进行拷贝,并执行压缩处理。
  2. G1中堆是由一个个region组成的,碎片问题比CMS少很多,碎片只会影响特定region,不会影响整个堆中老年代。
  3. CMS需要扫描整个堆进行对象存活判断,所以长时间停顿很常见,G1停顿时间取决于收集的region集合数量,而不是整个堆大小,所以相比cms长时间停顿少很多。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 春哥talk 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • JVM启动参数有哪些不合理的
  • 观察GC日志
  • 调优
  • 终极疗法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档