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

GC和垃圾回收器其四:一次JVM调参之旅

作者头像
春哥大魔王
发布2019-09-05 16:54:32
1.3K1
发布2019-09-05 16:54:32
举报

基础知识

HotspotVM垃圾回收采用分代回收算法,分代回收基于这样一个事实:对象生命周期不同,针对不同生命周期的对象可以采取不同的回收策略。

JVM堆分为:年轻代,年老代,永久代。

API采用:年轻代(ParNew),老年代(CMS)。

调优目标

调优主要目标围绕三个方向:内存占用,时延,吞吐,除此之外避免发生OOM,GC参数是否合理,启动速度等问题。

针对于API特点来说,主要关心两点:

  • 低延迟场景(gc频次,降低gc time响应影响)
  • 物尽其用(资源利用率)
  • 优化目标:降低TP99、TP90时间。

调优步骤

  • 确定目标
  • 优化参数
  • 验收结果

如何评估GC对TP99对影响

举个例子:

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

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

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

代码块

代码语言:javascript
复制
(50ms+25ms)× 100次/60000ms = 12.5%,50ms × 100次/60000ms = 8.3% 。

所以降低GC次数,降低GC时间可以有效降低tp999。

启动参数

首先从启动参数观察是否存在参数不合理的问题,参数如下:

代码块

代码语言:javascript
复制
-server
代码语言:javascript
复制
-Xmx6g
代码语言:javascript
复制
-Xms6g
代码语言:javascript
复制
#-Xmn1g
代码语言:javascript
复制
-XX:SurvivorRatio=8
代码语言:javascript
复制
-XX:NewRatio=2
代码语言:javascript
复制
-XX:PermSize=512m
代码语言:javascript
复制
-XX:MaxPermSize=1024m
代码语言:javascript
复制
代码语言:javascript
复制
-XX:+HeapDumpOnOutOfMemoryError
代码语言:javascript
复制
#-XX:ReservedCodeCacheSize=128m
代码语言:javascript
复制
#-XX:InitialCodeCacheSize=128m
代码语言:javascript
复制
代码语言:javascript
复制
-XX:+DisableExplicitGC
代码语言:javascript
复制
#-XX:+UseParallelGC
代码语言:javascript
复制
#-XX:ParallelGCThreads=4
代码语言:javascript
复制
#-XX:+UseParallelOldGC
代码语言:javascript
复制
#-XX:+UseAdaptiveSizePolicy
代码语言:javascript
复制
代码语言:javascript
复制
# -Dorg.apache.jasper.compiler.disablejsr199=true
代码语言:javascript
复制
# -Dcom.sun.management.jmxremote
代码语言:javascript
复制
# -Dorg.eclipse.jetty.util.log.IGNORED=true
代码语言:javascript
复制
# -Dorg.eclipse.jetty.LEVEL=DEBUG
代码语言:javascript
复制
# -Dorg.eclipse.jetty.util.log.stderr.SOURCE=true
代码语言:javascript
复制
# -verbose:gc
代码语言:javascript
复制
代码语言:javascript
复制
#-XX:+PrintGC
代码语言:javascript
复制
-XX:+PrintGCDetails
代码语言:javascript
复制
-XX:+PrintGCDateStamps
代码语言:javascript
复制
#-XX:+PrintGCTimeStamps
代码语言:javascript
复制
#-XX:+PrintGCApplicationConcurrentTime
代码语言:javascript
复制
#-XX:+PrintGCApplicationStoppedTime
代码语言:javascript
复制
#-XX:+PrintHeapAtGC
代码语言:javascript
复制
-XX:+PrintTenuringDistribution
代码语言:javascript
复制
-XX:+PrintCommandLineFlags
代码语言:javascript
复制
代码语言:javascript
复制
-XX:+UseConcMarkSweepGC
代码语言:javascript
复制
-XX:+UseParNewGC
代码语言:javascript
复制
-XX:ParallelCMSThreads=4
代码语言:javascript
复制
-XX:+CMSClassUnloadingEnabled
代码语言:javascript
复制
-XX:+UseCMSCompactAtFullCollection
代码语言:javascript
复制
-XX:CMSFullGCsBeforeCompaction=1
代码语言:javascript
复制
-XX:CMSInitiatingOccupancyFraction=50
代码语言:javascript
复制
-XX:+UseCMSInitiatingOccupancyOnly
代码语言:javascript
复制
#-----------------------------------------------------------

追加参数:

代码块

代码语言:javascript
复制
-XX:+ExplicitGCInvokesConcurrent
代码语言:javascript
复制
-XX:+PrintFlagsFinal
代码语言:javascript
复制
-XX:+PrintGCCause
代码语言:javascript
复制
-XX:+ScavengeBeforeFullGC
代码语言:javascript
复制
-XX:+CMSParallelRemarkEnabled
代码语言:javascript
复制
-XX:+CMSScavengeBeforeRemark
代码语言:javascript
复制
-XX:+CMSParallelInitialMarkEnabled
代码语言:javascript
复制
-XX:+ParallelRefProcEnabled
代码语言:javascript
复制
-XX:+AlwaysPreTouch
代码语言:javascript
复制
-XX:HeapDumpPath=/var/xxx/logs/xxx/xxx.heapdump.hprof$(date +%Y%m%d%H%M)

调参原则

通过GC日志或者falcon监控,观察GC情况,确定调优原则。这些原则作为调优基准或者突破口。

基于活跃数据的调优原则

基准:FullGC后老年代空间

堆:3-4倍基准(初始值和最大值相等)

新生代:1-1.5倍基准

老年代:2-3倍基准

永久代:1.2-1.5倍基准

观察perm区情况:

在不存在perm泄漏情况时,perm区1G够用了。

永久代稳定在650M-670M之间:

  • 设置最小768M,最大1G

FullGC之后有1.3G,设置2G old,85%触发为1.7G。年轻代3G,年老代2G。

4C8G Full GC之后大小:

Full GC之后老年代大小变为380M~390M。

8C16G Full GC之后大小:

Full GC之后老年代大小变为580M~590M。

Swap区大小:

4c8g:swap-2g

8c16g:swap-8g

非堆空间大小评估:

  • 午高峰期间线程数量400~600,留余buffer到1000,xss512k,留余其他agen等中间件内存使用+堆外内存考虑需要
  • 综上留余20%

整个堆大小设置:

  • 4C8G:8 * 0.8 - 1(+) = 5G(考虑内存碎片,取整)
  • 8C16G:16 * 0.8 - 1(+) = 10G(考虑内存碎片,取整)

老年代根据调优原则:

  • 4C8G:old:390M * 3 = 1.2G 考虑到CMS内存碎片,取整1.5G
  • 8C16G:old:590M * 3 = 1.7G 考虑到CMS内存碎片,取整2G

对应年轻代设置:

  • 4C8G:5G - 1.5G = 3.5G
  • 8C16G:10G - 2G = 8G

S区大小:

  • 4C8G:358M
  • 8C16G:819M

年轻代调优原则

如果Minor GC频繁通常是新生代空间较小,Eden很快填满,导致频繁Minor GC,所以可以通过增大新生代空间降低Minor GC频率。理论上相同内存分配前提下,新生代Eden增加一倍,Minor GC次数减少一半。

年轻代的Minor GC时间主要受哪些因素影响呢?

  • 扫描时间 T1
  • 复制时间 T2

扩容后Minor GC期间会增大T1的扫描时间,但可减少T2的制时间。对于JVM来说复制对象成本远高于扫描时间,所以在两个时间之中优先选择降低复制时间。所以单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小,所以如果堆中短生命对象较多,扩容年轻代,单次Minor GC时间不会增加。

同时扩容新生代之后,Minor GC次数频率降低,对象在年轻代得到充分回收,减少进入老年代的频率,这样也可以控制老年代的增速,Major GC自然会减少。

老年代调优原则

老年代原则比较简单,尽量将对象停留在新生代,降低老年代增长速率,整体降低Full GC造成的影响。直到一次Full GC Time时间处于接口容忍范围内。

了解下CMS触发Full GC的几种情况:

  • 大对象:大对象分配到老年代时,可用空间不足
  • 空间不足:perm或metaspace空间不足 (JDK 8 开始HotSpot取消了perm,将类信息存放在metaspace中)
  • 晋升失败:年轻代的存活对象,需要迁移到老年代时,老年代剩余对象不足
  • promotion failed:担保失败,,gc日志会记录信息(如:[ParNew (promotion failed): 1669947K->145784K(1887488K));
  • concurrent mode failure:执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的,gc日志会记录信息(如:(concurrent mode failure): 2902473K->1221894K(3354624K), 0.3778980 secs] )

调优之前GC日志:

调优后:

通过GC日志查看对象并未充分成长到指定年龄(15)就进入了老年代,也就造成了老年代增长迅速的事实。通过falcon观察在Full GC之后存活的对象不多,再次佐证了大量对象是短生命周期对象。

由于age并未达到目标值(15),所以可以认定为是担保失败造成了,所以反证通过调整年轻代大小可以达到老年代调优的目的。

动态年龄计算:

代码块

代码语言:javascript
复制
如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件:
代码语言:javascript
复制
a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。
代码语言:javascript
复制
b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。
代码语言:javascript
复制
代码语言:javascript
复制
相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题。

碎片问题

CMS的问题是碎片问题,老年代的问题也是碎片问题(连续可用内存空间不能容纳新晋升对象就触发fullgc了)。增大老年代可以在一定程度上解决碎片问题,但是悖论是增大老年代意味着老年代GC时间会变长。

代码块

代码语言:javascript
复制
-XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark // cmsgc之前进行一次ygc,减少堆扫描,两个参数一般配套使用

G1可以解决碎片问题,但是G1首先是适用于大堆,EAPI场景来说是否必须使用大堆呢?

调优结果

  • 115 4c8g 未调优
  • 116 4c8g 调优
  • 117 8c16g 调优(4倍量)

TP99

高峰期TP99对比:提升10ms~20ms

发版期间FullGC情况

  • 以前发版期间因为大量对象进入老年代触发一次fullgc
  • 调优后由于eden区及servivor区增大,不再触发fullgc

Gc Count / Gc Time

  • count降低50%
  • time由600ms降到200ms(分钟求和)

Full GC Count / Full GC Time

  • 由以前12小时一次Full GC变为43小时一次
  • Full GC time由60~70ms变为29ms

频次降低,时间降低。

结论

如果应用中存在大量短生命周期对象,可以选择较大的年轻代,如果存在较多的持久生命周期对象,老年代需要适当增大。


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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础知识
  • 调优目标
  • 调优步骤
    • 如何评估GC对TP99对影响
    • 启动参数
    • 调参原则
      • 基于活跃数据的调优原则
        • 年轻代调优原则
          • 老年代调优原则
            • 碎片问题
            • 调优结果
              • TP99
                • 发版期间FullGC情况
                  • Gc Count / Gc Time
                    • Full GC Count / Full GC Time
                      • 结论
                      相关产品与服务
                      消息队列 TDMQ
                      消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档