Java程序性能不达标,其他优化穷尽时,调整垃圾回收器来提高性能。但GC算法复杂,影响性能参数多。概要:
优化步骤: 明确目标→优化→跟踪结果。 案例 1、调eden大量短期对象,用较大年轻代
2、强制在Remark前gc,减少整体时间 3、固定Perm容量,避免扩容,发生full gc
一、优化方法
1、设置比例(例)
例如,GC日志获得老年代的活跃数据大小为300MB,那么各分区大小
2、确定目标
假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达
(50ms+25ms)/T请求受GC影响:GC前50ms内到达请求增加25ms,GC期间25ms内到达请求增加0-25ms不等,如发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间 还是次数N都减少GC对响应时间影响
3、优化
如选回收器、设置内存比例、调整JVM参数等
本文案例垃圾回收器均为ParNew+CMS,CMS失败时Serial Old替补
案例一:Major GC和Minor GC频繁
Minor GC 100次/分,单次25ms,Major GC 4分钟一次,单次200ms,接口响应时间50ms
服务要求低延时高可用,Minor GC,12.5%请求响应时间会增加,其中8.3%的请求响应时间会增加25ms,影响较大。(50ms+25ms)× 100次/60000ms = 12.5%,50ms × 100次/60000ms = 8.3% 。
优化目标:降低TP99、TP90时间
1、优化
(1)Eden大小:Minor GC频繁:通常由于新生代空间小,Eden区很快被填满,Eden增加一倍,Minor GC减少一半。但单次Minor GC时间也可能增加
单次时间两部分:T1(扫描新生代)和 T2(复制存活对象到Survivor区)
(2) 1)扩容前:新生代容量为R ,对象存活时间为750ms,Minor GC间隔500ms,本次GC时间= T1(扫描新生代R)+T2(复制对象A到S)。
2)扩容后:容量为2R ,GC间隔1000ms,A不再存活,不需复制,时间 = 2 × T1(扫新生代R),没有时间。
复制成本远高于扫描,单次Minor GC时间取决于GC后存活对象数量,非Eden区大小。如堆中短期对象多,扩容新生代,单次Minor GC时间不会显著增加。下面要确认服务中对象的生命周期分布:
(3)上图GC日志中红色可知:
1)new threshold = 2:2次Minor GC后就晋升到老年代,老年代满,频繁Major GC
2)GC后老年代空间300Mb+,意味大多数(86% = 2G/2.3G)不活,生命周期长的对象少
2、优化结果
新生代为原来三倍,单次Minor GC时间增加小于5ms,频率下降60%,服务响应时间TP90,TP99都下降了10ms+
小结:大量短期对象,用较大年轻代;较多持久对象,老年代适当增大。
3、动态年龄计算
MaxTenuringThreshold=15,对象仍2次Minor GC,就到老年代?
Hotspot遍历所有对象时,年龄超过survivor区一半时,取年龄和MaxTenuringThreshold中更小值,作为晋升年龄阈值。本例中,调优前:Survivor = 64M,desired survivor = 32M(目标使用率),age<=2为41M,大于32M,所以设置为2。
引入原因动态适应变化:
a)MaxTenuringThreshold过大,应该晋升在Survivor区,Survivor区溢出,Eden+Svuvivor全部到老年代,老化机制失效
b)MaxTenuringThreshold过小,大量短对象到老年代,频繁Major GC。分代回收失去意义
案例二:请求高峰期发生GC,导致服务可用性下降
确定目标:降低Remark时间,高峰期CMS在重标记(Remark)阶段耗时1.39s。Remark阶段Stop-The-World,
1、优化
1、Remark阶段仅扫描老年代是否可行?不可行
跨代引用:对象A因为引用在新生代,Remark时不会被修正标记为可达,GC时会被错误回收。
Remark阶段必须扫描整个堆来判断对象是否存活,包括图中灰色的不可达对象。
分析GC日志得出,Remark耗时>500ms时,新生代使用率在75%以上。
2、减少新生代对象数量
1)CMS在Remark前增加可中断并发预清理(CMS-concurrent-abortable-preclean),Eden区使用超过2Mb启动,使用率50%中断(2Mb、50%默认阈值),如此阶段执行时Minor GC,灰色对象将被回收,Remark扫描就少
2)避免没到Minor GC陷入无限等待,CMSMaxAbortablePrecleanTime = 5s,不管发没发生Minor GC,都中止此阶段,进入Remark。
例:红色标记2处,并发预清理5.35s,被中断,没等到Minor GC ,Remark时新生代中仍很多对象。
3)对于这种情况,CMSScavengeBeforeRemark参数,Remark前强制一次Minor GC
优化结果:GCtime和业务波动保持一致,不再有明显的毛刺。
3、如何避免Minor GC扫描全堆?
Minor GC时必须扫老年代,因为老年代可能有新生代引用。
老持新情况不足1%,JVM引入了卡表(card table)老年代空间分成若干个512B卡(单字节数组),老引新时,设置卡表元素。之后Minor GC扫描很快识别
主案例三:发生Stop-The-World的GC
目标:Full GC耗时1.23s,降低单次STW时间
1、触发STW的Full GC原因:
1)Perm空间不足(JDK8开始,Perm完全消失,用元空间,直接存内存中,不在JVM)
2) promotion failed和concurrent mode failure发生原因一般是CMS进行,但老年代不足,尽快回收老年代,STW,进行Serial Old GC;
3) 新晋升到老年代大小>老年代剩余空间
4)触发Full GC(执行jmap -histo:live [pid])避免碎片
排除原因2:原因2中两种情况,日志中会有特殊标识(当前没有)。
排除原因3:根据GC日志,老年代仅用20%,没有大于2G对象
排除原因4:没执行命令
2、锁定原因1,两种解决方法:
1)-XX:PermSize和-XX:MaxPermSize设置成一样,启动强制 固定永久带容量,减少内存自动扩容和收缩的性能损失
2)CMS默认不会回收Perm区,通过CMSPermGenSweepingEnabled、CMSClassUnloadingEnabled ,让CMS在Perm区容量不足时对其回收
由于没生成大量动态类,回收Perm区收益不大,用方案1,Perm区大小固定,避免进行动态扩容。
https://mp.weixin.qq.com/s/t1Cx1n6irN1RWG8HQyHU2w