当前位置:首页 > 经验

这应该是全网最全的CMS学习笔记了-cms教程大全

引子带着问题去学习一个东西,才会有目标感,我先把一直以来自己对CMS的有的疑惑罗列了下,渴望这一篇学习笔记能解决掉这些疑惑,渴望也能对你有所帮助。

CMS出现的初心、背景和目的?CMS的适合用场景?CMS的trade-off是什么?优势、劣势和代价CMS会回收哪个地区的对象?CMS的GC Roots包括那些对象?CMS的过程?CMS和Full gc是不是一回事?CMS何时触发?CMS的日志怎么样解析?CMS的调优怎么样做?CMS扫描那些对象?CMS和CMS collector的区别?CMS的介绍参数设置?为什么ParNew可以和CMS搭配使用,而Parallel Scanvenge不可以?一、基本知识CMS获得器:Mostly-Concurrent获得器,也称并发标记清除获得器(Concurrent Mark-Sweep GC,CMS获得器),它管理新生代的方法与Parallel获得器和Serial获得器相同,而在老时代则是尽可能得并发执行,每一个垃圾获得器周期只有2次短停顿。我以前对CMS的理解,以为它是针对老时代的获得器。今天查阅了《Java性能优化权威指南》和《Java性能权威指南》两本书,确认以前的理解是错误的。CMS的初心和目的:为了消除Throught获得器和Serial获得器在Full GC周期中的很长时间停顿。CMS的适合用场景:如果你的应用需要更快的响应,不渴望有很长时间的停顿,同一时间你的CPU资源也比较多姿多彩,就适适合用CMS获得器。二、CMS的过程CMS的正常过程

这里我们首先就这样看下CMS并发获得周期正常完成的几个情况。

(STW)初始标记:这种阶段是标记从GcRoots直接可达的老时代对象、新生代引用的老时代对象,就是下图中灰色的点。这种过程是单线程的(JDK7以前单线程,JDK8之后并行,可以通过参数CMSParallelInitialMarkEnabled修改)。

初始标记标记的对象

并发标记:由上一个阶段标记过的对象,开始tracing过程,标记任何可达的对象,这种阶段垃圾回收线程和应用线程同一时间运行,如上图中的灰色的点。在并发标记过程中,应用线程还在跑,因此会导致有的对象会从新生代晋升到老时代、有的老时代的对象引用会被变化、有的对象会直接分配到老时代,这些受到波及的老时代对象所在的card会被标记为dirty,用来从头开始标记阶段扫描。这种阶段过程中,老时代对象的card被标记为dirty的可能原因,就是下图中绿帽色的线:

并发标记过程中受到波及的对象

预清理:预清理,也是用来标记老时代存活的对象,目的是为了让从头开始标记阶段的STW尽可能短。这种阶段的目标是在并发标记阶段被应用线程波及到的老时代对象,包括:(1)老时代中card为dirty的对象;(2)幸存区(from和to)中引用的老时代对象。因此,这种阶段也需要扫描新生代+老时代。【PS:会不会扫描Eden区的对象,我就这样看源代码猜测是没有,还需要继续求证】

预清理中扫描from和to区

可中断的预清理:这种阶段的目标跟“预清理”阶段相同,也是为了减少从头开始标记阶段的事情量。可中断预清理的价值:在进入从头开始标记阶段以前尽量等到一个Minor GC,尽量缩短从头开始标记阶段的停顿时光。另外可中断预清理会在Eden达到50%的时候开始,这时候离下一次minor gc还有半程的时光,这种还有另外一个意义,即避免短暂的时间内连着的两个停顿,如下图资料所示:

避免连续停顿的发生

在预清理步骤后,如果满足下面两个条件,就不会开启可中断的预清理,直接进入从头开始标记阶段:

Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这种参数的默认值是2M;Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这种参数的默认值是50%。如果不满足上面两个条件,则进入可中断的预清理,可中断预清理可能会执行多次,那么退出这种阶段的出口有两个(源码参见下图):

设置了CMSMaxAbortablePrecleanLoops,并且执行的次数超过了这种值,这种参数的默认值是0; CMSMaxAbortablePrecleanTime,执行可中断预清理的时光超过了这种值,这种参数的默认值是5000毫秒。

可中断预清理退出的条件

如果是因为这种原因退出,gc日志打印如下:

可中断预清理由于时光退出

有可能可中断预清理过程中一直没等到Minor gc,这时候进入从头开始标记阶段的话,新生代还有非常多活着的对象,就回导致STW变长,因此CMS还提供了CMSScavengeBeforeRemark参数,可以在进入从头开始标记以前强烈进行依次Minor gc。

(STW)从头开始标记:从头开始扫描堆中的对象,进行可达性解析,标记活着的对象。这种阶段扫描的目标是:新生代的对象 + Gc Roots + 前面被标记为dirty的card对应的老时代对象。如果预清理的事情没做好,这一步扫描新生代的时候就会花非常多的时间,导致这种阶段的停顿时光过长。这种过程是多线程的。并发清除:玩家线程被从头开始激活,同一时间将那些未被标记为存活的对象标记为不可达;并发重置:CMS内部重置回收器情况,准备进入下一个并发回收周期。CMS的不正常状态上面描述的是CMS的并发周期正常完成的状态,但是还有几种CMS并发周期失败的状态:

并发模式失败(Concurrent mode failure):CMS的目标就是在回收老时代对象的时候不要终止全部应用线程,在并发周期执行期间,玩家的线程依然在运行,如果这时候如果应用线程向老时代请求分配的空间超过预留的空间(担保失败),就回触发concurrent mode failure,之后跟着CMS的并发周期就会被一次Full GC代替——终止全部应用进行垃圾获得,并进行空间压缩。如果我们设置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,之中CMSInitiatingOccupancyFraction的值是70,那预留空间就是老时代的30%。晋升失败:新生代做minor gc的时候,需要CMS的担保机制确认老时代是否有足够的空间容纳要晋升的对象,担保机制发现不够,则报concurrent mode failure,如果担保机制判断是够的,但是事实上由于碎片问题导致无法分配,就会报晋升失败。永久代空间(或Java8的元空间)耗尽,默认状态下,CMS不会对永久代进行获得,只要永久代空间耗尽,就回触发Full GC。三、CMS的调优针对停顿时光过长的调优 首先需要判断是哪个阶段的停顿导致的,之后跟着再针对详细的原因进行调优。使用CMS获得器的JVM可能引发停顿的状态有:(1)Minor gc的停顿;(2)并发周期里初始标记的停顿;(3)并发周期里从头开始标记的停顿;(4)Serial-Old获得老时代的停顿;(5)Full GC的停顿。之中并发模式失败会导致第(4)种状态,晋升失败和永久代空间耗尽会导致第(5)种状态。针对并发模式失败的调优想到一个办法增大老时代的空间,增加整个堆的大小,或者减少年轻代的大小以更高的频率执行后台的回收线程,即提升CMS并发周期发生的频率。设置UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,调低CMSInitiatingOccupancyFraction的值,但是也不可以调得太低,太低了会导致过多的无效的并发周期,会导致消耗CPU时光和再多的无效的停顿。一般来讲,这种过程需要几个迭代,但是还是有一定的套路,参见《Java性能权威指南》中给出的反馈,摘抄如下:对特殊的应用软件程序,该标志的更优值可以根据 GC 日志中 CMS 周期首次启动失败时的值获得。详细途径是,在垃圾回收日志中找到并发模式失效,寻找后再反向查找 CMS 周期最近的启动记录,之后跟着根据日志来计算这时候的老时代空间占用值,之后跟着设置一个比该值更小的值。增多回收线程的个数 CMS默认的垃圾获得线程数是(CPU个数 + 3)/4,这种公式的含义是:当CPU个数大于4个的时候,垃圾回收后台线程至少占用25%的CPU资源。举个举例:如果CPU核数是1-4个,那么会有1个CPU用来垃圾获得,如果CPU核数是5-8个,那么久会有2个CPU用来垃圾获得。

针对永久代的调优

如果永久代需要垃圾回收(或元空间扩容),就会触发Full GC。默认状态下,CMS不会处理永久代中的垃圾,可以通过开启CMSPermGenSweepingEnabled配置来开启永久代中的垃圾回收,开启后会有一组后台线程针对永久代做获得,需要小心的是,触发永久代进行垃圾获得的指标跟触发老时代进行垃圾获得的指标是独立的,老时代的阈值可以通过CMSInitiatingPermOccupancyFraction参数设置,这种参数的默认值是80%。开启对永久代的垃圾获得只是之中的一步,还需要开启另外一个参数——CMSClassUnloadingEnabled,使得在垃圾获得的时候可以卸载不用的类。

四、CMS的trade-off是什么?优势

低延迟的获得器:几乎没有很长时间的停顿,应用软件程序只在Minor gc以及后台线程扫描老时代的时候发生极其短暂的停顿。劣势

更高的CPU使用:一定有足够的CPU资源用来运行后台的垃圾获得线程,在应用软件程序线程运行的同一时间扫描堆的使用状态。【PS:现在服务器的CPU资源基础不是问题,这种点可以忽略】CMS获得器对老时代获得的时候,不再进行所有压缩和整理的事情,说明着老时代随着应用的运行会变得碎片化;碎片过多会波及大对象的分配,即便老时代还有蛮大的剩余空间,但是没有连续的空间来分配大对象,这时候就会触发Full GC。CMS提供了两个参数来解决这种问题:(1)UseCMSCompactAtFullCollection,在要进行Full GC的时候进行内存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不压缩的Full GC后,执行一次带压缩的Full GC。会出现浮动垃圾;在并发清理阶段,玩家线程依然在运行,一定预留出空间给玩家线程使用,因此CMS比很多回收器需要更大的堆空间。五、几个问题的解答为什么ParNew可以和CMS搭配使用,而Parallel Scanvenge不可以?

答:这种跟Hotspot VM的简史有关,Parallel Scanvenge是不在“分代框架”下研究的,而ParNew、CMS都是在分代框架下研究的。

CMS中minor gc和major gc是顺序发生的吗?

答:不是的,可以交叉发生,即在并发周期执行过程中,是可以发生Minor gc的,这种找个gc日志就可以研究到。

CMS的并发获得周期适合触发?

由下图可以就这样看出,CMS 并发周期触发的条件有两个:

触发cms并发周期的条件

阈值检查机制:老时代的使用空间达到某个阈值,JVM的默认值是92%(jdk1.5以前是68%,jdk1.6之后是92%),或者可以通过CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly两个参数来设置;这种参数的设置需要就这样看应用场景,设置得太小,会导致CMS频繁发生,设置得太大,会导致过多的并发模式失败。比如

动态检查机制:JVM会根据最近的回收简史,估算下一次老时代被耗尽的时光,快到这种时光的时候就启动一个并发周期。设置UseCMSInitiatingOccupancyOnly这种参数可以将这种特性关闭。

CMS的并发获得周期会扫描哪些对象?会回收哪些对象?

答:CMS的并发周期只会回收老时代的对象,但是在标记老时代的存活对象时,可能有的对象会被年轻代的对象引用,因此需要扫描整个堆的对象。

CMS的gc roots包括哪些对象?

答:首先,在JVM垃圾获得中Gc Roots的概念怎么样理解(参见R大对GC roots的概念的解答);第二,CMS的并发获得周期中,怎么样判断老时代的对象是活着?我们前面提到了,在CMS的并发周期中,仅仅扫描Gc Roots直达的对象会有遗漏,还需要扫描新生代的对象。如下图中的蓝色字体所示,CMS中的年轻代和老时代是分别获得的,因此在判断年轻代的对象存活的时候,需要把老时代当作自己的GcRoots,这时候并不需要扫描老时代的全部对象,而是使用了card table资料结构,如果一个老时代对象引用了年轻代的对象,则card中的值会被设置为特定的数值;反过来判断老时代对象存活的时候,也需要把年轻代当作自己的Gc Roots,这种过程我们在第三节已经论述过了。

老时代和新生代互相作为Gc Roots

如果我的应用决定使用CMS获得器,介绍的JVM参数是什么?我自己的应用使用的参数如下,是根据PerfMa的xxfox生成的,各位也完全可以使用这种业务调优自己的JVM参数:

CMS有关的参数总结(需要小心的是,这里我没有思考太多JDK版本的问题,JDK1.7和JDK1.8这些参数的配置,有的默认值可能不一样,详细使用的时候还需要根据详细的版本来确认怎么去调配设置)

声明:此文信息来源于网络,登载此文只为提供信息参考,并不用于任何商业目的。如有侵权,请及时联系我们:fendou3451@163.com
标签:

  • 关注微信

相关文章