HotspotVM垃圾回收采用分代回收算法,分代回收基于这样一个事实:对象生命周期不同,针对不同生命周期的对象可以采取不同的回收策略。
JVM堆分为:年轻代,年老代,永久代。
API采用:年轻代(ParNew),老年代(CMS)。
调优主要目标围绕三个方向:内存占用,时延,吞吐,除此之外避免发生OOM,GC参数是否合理,启动速度等问题。
针对于API特点来说,主要关心两点:
举个例子:
服务情况: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情况对响应时间影响较大。
代码块
(50ms+25ms)× 100次/60000ms = 12.5%,50ms × 100次/60000ms = 8.3% 。
所以降低GC次数,降低GC时间可以有效降低tp999。
首先从启动参数观察是否存在参数不合理的问题,参数如下:
代码块
-server
-Xmx6g
-Xms6g
#-Xmn1g
-XX:SurvivorRatio=8
-XX:NewRatio=2
-XX:PermSize=512m
-XX:MaxPermSize=1024m
-XX:+HeapDumpOnOutOfMemoryError
#-XX:ReservedCodeCacheSize=128m
#-XX:InitialCodeCacheSize=128m
-XX:+DisableExplicitGC
#-XX:+UseParallelGC
#-XX:ParallelGCThreads=4
#-XX:+UseParallelOldGC
#-XX:+UseAdaptiveSizePolicy
# -Dorg.apache.jasper.compiler.disablejsr199=true
# -Dcom.sun.management.jmxremote
# -Dorg.eclipse.jetty.util.log.IGNORED=true
# -Dorg.eclipse.jetty.LEVEL=DEBUG
# -Dorg.eclipse.jetty.util.log.stderr.SOURCE=true
# -verbose:gc
#-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
#-XX:+PrintGCTimeStamps
#-XX:+PrintGCApplicationConcurrentTime
#-XX:+PrintGCApplicationStoppedTime
#-XX:+PrintHeapAtGC
-XX:+PrintTenuringDistribution
-XX:+PrintCommandLineFlags
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:ParallelCMSThreads=4
-XX:+CMSClassUnloadingEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=1
-XX:CMSInitiatingOccupancyFraction=50
-XX:+UseCMSInitiatingOccupancyOnly
#-----------------------------------------------------------
追加参数:
代码块
-XX:+ExplicitGCInvokesConcurrent
-XX:+PrintFlagsFinal
-XX:+PrintGCCause
-XX:+ScavengeBeforeFullGC
-XX:+CMSParallelRemarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+CMSParallelInitialMarkEnabled
-XX:+ParallelRefProcEnabled
-XX:+AlwaysPreTouch
-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之间:
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
非堆空间大小评估:
整个堆大小设置:
老年代根据调优原则:
对应年轻代设置:
S区大小:
如果Minor GC频繁通常是新生代空间较小,Eden很快填满,导致频繁Minor GC,所以可以通过增大新生代空间降低Minor GC频率。理论上相同内存分配前提下,新生代Eden增加一倍,Minor GC次数减少一半。
年轻代的Minor GC时间主要受哪些因素影响呢?
扩容后Minor GC期间会增大T1的扫描时间,但可减少T2的制时间。对于JVM来说复制对象成本远高于扫描时间,所以在两个时间之中优先选择降低复制时间。所以单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小,所以如果堆中短生命对象较多,扩容年轻代,单次Minor GC时间不会增加。
同时扩容新生代之后,Minor GC次数频率降低,对象在年轻代得到充分回收,减少进入老年代的频率,这样也可以控制老年代的增速,Major GC自然会减少。
老年代原则比较简单,尽量将对象停留在新生代,降低老年代增长速率,整体降低Full GC造成的影响。直到一次Full GC Time时间处于接口容忍范围内。
了解下CMS触发Full GC的几种情况:
调优之前GC日志:
调优后:
通过GC日志查看对象并未充分成长到指定年龄(15)就进入了老年代,也就造成了老年代增长迅速的事实。通过falcon观察在Full GC之后存活的对象不多,再次佐证了大量对象是短生命周期对象。
由于age并未达到目标值(15),所以可以认定为是担保失败造成了,所以反证通过调整年轻代大小可以达到老年代调优的目的。
动态年龄计算:
代码块
如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件:
a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。
b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。
相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题。
CMS的问题是碎片问题,老年代的问题也是碎片问题(连续可用内存空间不能容纳新晋升对象就触发fullgc了)。增大老年代可以在一定程度上解决碎片问题,但是悖论是增大老年代意味着老年代GC时间会变长。
代码块
-XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark // cmsgc之前进行一次ygc,减少堆扫描,两个参数一般配套使用
G1可以解决碎片问题,但是G1首先是适用于大堆,EAPI场景来说是否必须使用大堆呢?
高峰期TP99对比:提升10ms~20ms
频次降低,时间降低。
如果应用中存在大量短生命周期对象,可以选择较大的年轻代,如果存在较多的持久生命周期对象,老年代需要适当增大。