java垃圾回收——CMS
what:
CMS全称Concurrent marke sweep,中文是并发标记清除算法。
CMS出现的目的是:尽可能的减少STW(stop the world)的时间。
大体流程:
注意:“预清理”和“可终止清理”没有标记出来
how:
CMS工作分7步,分别是:
1、初始标记;
STW的方式工作。标记出根对象(GC root直接引用的对象),从线程栈、静态区(方法区的静态属性引用对象、方法区的常量引用对象)、本地方法栈的JNI引用的对象。
包含2部分:a、老年代中全部的GC root对象,如下图的1结点;b、年轻代中活着的对象,它们所引用到的老年代中的对象,如下图的节点2、3
2、并行标记;
以“初始标记”的对象集为起点,并行的找到所有存活的对象;由于该阶段是和用户线程同时进行的,所以对象之间的引用关系会发生改变(如:新生代升到老年代、老年代中的对象引用关系变化等)。如果不进行后续处理,会出现“漏标”的情况。即有了后面的“重新标记”步骤。
为了提高“重新标记”的效率,会将“并行标记”阶段变化的对象所在的card标记为dirty,后续只扫描标记为dirty的card。
该阶段会增加2个节点4和5。
3、预清理;
由于“并发标记”阶段,并不能标记所有存活的对象(原因是:并行)。在该阶段会扫描所有dirty的card。如下图:“并发标记”阶段,节点3的引用关系发生了变化,所有节点3所在的card会被标记成dirty。
扫描后,6节点会被标记上,如下图;
4、可终止预清理;
该阶段主要是为下阶段的工作铺垫,尽量承担一部分下阶段的工作,该阶段持续的事件依赖很多因素,例如:重复次数、持续工作时间登。
目的是:期待在该阶段中发生异常YGC,从而减少下个阶段扫描新生代对老年代引用的时间。
5、重新标记;
目的:标记老年代中所有存活的对象。
本阶段标记的范围是:整个堆,包含young和old代。为何扫描young代?old代中的对象只要是被young代中的对象引用了,就算是活的。即使young中的对象是不可达,也会用来做gc root扫描old代。
在第2到第4阶段,都是和用户线程并行的,young代中对old代中的对象引用关系发生了变化,所以remark要化很长时间来STW做标记。则CMS会尽量在做final remark时,young代是足够干净的。
可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc。
6、并发清理
此阶段是:完成前5阶段标记后剩下的垃圾对象。
在此阶段的时间内,用户新产生的垃圾,就只能等到下个阶段在清理了。
7、并发重置;
重置CMS内部的数据结构,准备给下个阶段的CMS使用。
注意:
1、减少final remark的时间:
一般CMS的80%时间来自“final remark”,如果发现该阶段的时间比较长,可以考虑使用如下参数:
-XX:+CMSScavengeBeforeRemark
2、内存碎片:
CMS是基于“标记-清理”原理来做的。
一般通过:CMSFullGCsBeforeCompaction来设计压缩的。CMSFullGCsBeforeCompaction默认是0。
3、concurrent mode failure
CMS除了“初始标记”和“重新标记”2个阶段会STW,其他阶段都是和用户进程并发的。就会出现:用户线程向老年代不断提交新的对象,但是老年代还没来得及腾出空间,老年代空间满了,就出现了concurrent mode failure。
可以通过调整:-XX:CMSInitiatingOccupancyFraction和-XX:+UseCMSInitiatingOccupancyOnly。二者关系:如果不设置UseCMSInitiatingOccupancyOnly,只设置XX:CMSInitiatingOccupancyFraction。那么JVM只会第一次使用XX:CMSInitiatingOccupancyFraction,后面会自动调整XX:CMSInitiatingOccupancyFraction,从而导致设置的不在有效。
注意:如果XX:CMSInitiatingOccupancyFraction设置大了会fullGC,小了会频繁cms gc,那就说明老年代的空间小了(需要增大)。
危害:“concurrent mode failure”会导致fullGC变成Serial Old收集器,它是单线程的标记-压缩收集器,所以耗时非常的长。
如下面的例子:
4、promotion failed
发生在年轻代回收。一般是:Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的。
解决方案:a、增大Survivor空间;b、减少对象的大小;