JVM垃圾回收和调优实践

JVM垃圾回收

YGC、OldGC、FullGC区别和联系

区别

YGC ->Minor GG:发生在年轻代的GC

OldGC -> Major GC:发生在老年代的GC

FullGC -> Full GC:发生在年轻代、老年代、方法区的GC(方法区VS持久代:方法区是JVM规范,持久代是方法区的实现,jdk8后方法区由本地内存实现)

 

​                                           ​

联系

发生FullGC一定会发生了YGC和OldGC。

除了CMS,其它垃圾回收器发生了OldGC也一定会发生FullGC(CMS是唯一能不经过fullgc触发回收年老代的回收器)。

发生YGC不一定发生OldGC和FullGC。

科普:jmx和jstat里的full gc次数为什么统计得不同?
1、jmx统计的是oldgc的次数
2、jstat统计的是oldgc停顿的时间,在CMS中有两次停顿,所以通过jstat统计出来的fgc次数会是cms oldgc的两倍。

 

各种GC发生的条件

YGC发生的条件:

  • eden区不足容纳新对象(注意不是survivor区不足,survivor区不足能引起的是OldGC或者FullGC)

  • FullGC发生的时候

 

OldGC发生的条件:

  • FullGC发生的时候

  • CMS下使用内存超过阀值的时候(CMS GC是唯一一个可以不需要通过FullGC来触发OldGC的回收器,G1是MixedGC,不算真正的OldGC)

FullGC发生的条件:

  • 没有开启DisableExplicitGC的情况下调用System.gc();

  • 老年代是否有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,如果不够的话,就提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败(主动gc);

  • 开启-Xnoclassgc参数下方法区的空间不足;

  • MinorGC后,要晋升到old区的对象大小大于old区的大小(promotion failed)(被动gc);

  • CMS GC出现concurrent mode failure时候,通过Full GC引起一次Serial Old GC。

 

一个对象的正常生命历程

eden -> survivor1区 -> survivor2区 -> survivor1区.... -> old区

可以通过参数设置不经过survivor区:
1、MaxTenuringThreshold=0:设置晋升年龄为0
2、PretenureSizeThreshold:大对象直接进入老年代(只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数)

什么情况下的对象会进到old区?

条件1:达到设定的晋升年龄

条件2:达到surivivor区大小的一半

​                                           ​

正常历程里面,有没有可能出现不经过survivor区的历程?

答案是有。

发生promotion failed的时候,这个时候会触发fullGC。

 

CMS的垃圾回收过程

​                                           ​

 

CMS Initital Mark:初始化标记

   这个阶段会扫描root对象直接关联的可达对象(老年代的对象)。注意不会递归的追踪下去,只是到达第一层而已。这个过程,会STW,但是时间很短。​                                           ​

CMS-Concurrent-mark-start和CMS-Concurrent-mark:并发标记

该阶段GC线程和应用线程并发执行,遍历第一阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。

因为该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。

为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代。

​                                           ​

CMS-concurrent-preclean-start和CMS-concurrent-preclean:预处理

 通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用。那些能够从dirty card对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了

CMS-concurrent-abortable-preclean-start和CMS-concurrent-abortable-preclean:可中断预处理

 该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

在该阶段,主要循环的做两件事:

  1. 处理 From 和 To 区的对象,标记可达的老年代对象

  1. 和上一个阶段一样,扫描处理Dirty Card中的对象

为什么需要这个阶段,存在的价值是什么?

因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

CMS Final Remark:并发标记

  1. 遍历新生代对象,重新标记

  1. 根据GC Roots,重新标记

  1. 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在clean阶段处理过

在第一步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在AbortablePreclean阶段中能够恰好的发生一次YGC,这样就可以避免扫描无效的对象。

如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?

CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。

不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。

所以利弊需要把握。

CMS-concurrent-sweep-start和CMS-concurrent-sweep:并发清除

对标记的对象进行清除,会产生内存碎片。

CMS-concurrent-reset-start和CMS-concurrent-reset:重置

     为下一次CMSGC做准备

 

调优实践

(由于程序不当造成的gc问题不在这里讨论,因为涉及到gc日志分析)

FullGC停顿时间长

主要停顿时间是重新标记,所以关键是减少重新标记的时间:

1.因为重新标记会扫描年轻代,所以可以减少年轻代的扫描标记

  • 减少年轻代大小,减少扫描空间(不优先建议,因为减少年轻代可能引起频繁ygc或fullgc)

  • 重新标记之前进行一次ygc,减少ygc里可标记的对象(添加参数CMSScavengeBeforeRemark,默认是关闭)

2.减少老年代的扫描标记

  • 减少老年代回收对象的空间大小(调小阀值或减少old内存大小,内存允许下先考虑调小阀值)

  • 添加参数CMSPrecleaningEnabled开启预清理(默认是开启),在和用户线程并发的情况下尽可能的标记对象

3.开启并行模式,减少STW时间:添加参数-XX:+CMSParallelRemarkEnabled(默认是开启)

 

案例:资讯系统

问题现象:服务响应超时

分析:fgc频率每25天一次,每次fullgc时间约3分钟,其中重新标记时间86s

调优:阀值92%->50%

结果:频率从25天/次->11天/次,fullgc时间约12s,重新标记时间86s -> 6s)

 

FullGC频繁

1.增大老年代

  • 调大阀值(适用阀值小的情况,需关注停顿时间)

  • 加大堆内存,加大老年代内存(需关注停顿时间)

  • 堆内存不变,减少年轻代(需关注ygc频率)

 

案例:主站服务

问题现象:tair上线后,fgc频率由原来的每30+分钟一次下降到每20+分钟一次

分析:

1.ygc频繁,每1秒1~2次,每次时间约18ms,每秒晋升老年代空间大小约3M

2.fgc时间正常,每次总共时间约5~10s

3.触发old gc的阀值为92,阀值已经够大

调优:加大堆内存,加大年轻代和老年代内存大小

结果:

1.ygc频率降为每1秒0.5~1次

​                                           ​

2.fullgc恢复到原来的大于每30分钟一次(20:38:12 ~ 21:21:46),fgc时间约8秒,两次停顿时间不足1s,主要时间花费在concurrent-sweep(是否说明有很多大对象?page faults相关?).

​                                           ​

 

 

promotion failed引起的FullGC停顿时间长

原因:进行MinorGC时,SurvivorSpace放不下,对象只能放入老年代,而此时老年代也放不下便会引起promotion failed。

方案1.增大surivor区的大小

方案2.增大老年代剩余空间

方案3.整理老年代的内存碎片(参数-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5)

案例:建档服务

问题现象:建档服务不时出现TCP连接异常的告警,查看日志发生了因promotion failed而引起的fullgc,fullgc停顿时间约1.5分钟。

​                                           ​

分析:​                                           ​

1.默认已经启用来了内存压缩,所以没有内存压缩问题(-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=0);

2.survivor的空间使用率比较低,基本维持在10%~30%之间:

​                                           ​

调大survivor会浪费空间

3.阀值CMSInitiatingOccupancyFraction使用默认值92%,发生promotion failed时老年代剩余空间约10%。full gc频率为1天一次,调小CMSInitiatingOccupancyFraction正常不会引起频繁FullGC.

调优:

降低CMSInitiatingOccupancyFraction=70,增加CMS GC发生时剩余的可用空间。

效果:

fullgc频率相差不大(约一天一次),fgc时间约34秒,两次停顿时间总共约16秒。

​                                           

concurrent mode failure(并发模式失败)

原因:在CMS GC过程中,对象晋升到老年代的大小大于Old区可用大小从而发生的情况。发生了concurrent mode failure后退化成Serial Old GC,停顿用户进程,所以concurrent mode failure一般出现的时候影响用户会比较严重。

方案:

1.增加老年代可用空间->调小阀值

2.降低fgc时间,降低发生概率

 

参考文档:https://www.jianshu.com/p/2a1b2f17d3e4

 

你可能感兴趣的:(JVM)