Concurrent Mark Sweep
,是一款基于并发、使用标记清除算法的垃圾回收算法,只针对老年代进行垃圾回收。CMS收集器工作时,GC工作线程和用户线程可以并发执行,以达到降低STW
时间的目的。
开起VM选项-XX:+UseConcMarkSweepGC
,表示对老年代的回收采用CMS。
生产环境中常用的两种垃圾收集器(ParNew:年轻代,CMS:老年代)
根据GC的触发机制分为:周期性Old GC(被动)和主动Old GC
一般都是被动GC,这里主要说的也是这个。
主动Old GC的过程,触发条件比较苛刻:
System.gc()
,前提是没有参数ExplicitGCInvokesConcurrent
如果触发了主动Old GC
,这时周期性Old GC
正在执行,那么会夺过周期性Old GC
的执行权(同一个时刻只能有一种在Old GC
在运行),并记录 concurrent mode failure
或者 concurrent mode interrupted
。
首先,我们需要厘清一个概念,即只有标记
阶段才需要STW (Stop The World)
。标记完成以后,需要清除的对象已经确定,无论此时是否产生新的垃圾,都不影响对这些对象的清理。也就是说,清除
阶段是可以设计成和用户线程并发执行的。
JVM在暂停的时候,需要选准一个时机,由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点(safepoint)
的概念:程序只有在运行到安全点的时候,才可以暂停下来。HotSpot
采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起。HotSpot
使用了几条短小精炼的汇编指令便可完成安全点轮询以及触发线程中断,因此对系统性能的影响几乎可以忽略不计。
可达性
是指,如果一个对象会被至少一个程序中的可达对象通过直接或间接的方式引用,则称该对象是可达的
。更详细地说,一个对象满足一下两个条件之一,即被判定为可达的。
1.本身是根对象。根(root)是指由堆以外空间访问的对象。JVM会将以下对象标记为根:a.虚拟机栈(栈帧中的本地变量表)中引用的对象;b.方法区中的类静态属性引用的对象;c.方法区中的常量引用的对象;d.本地方法栈中JNI的引用对象。
2.被一个可达的对象引用。
CMS
将可达性分析分解成两个阶段:a.仅扫描与根节点直接关联的对象; b.继续向下扫描完所有对象。因此,标记
阶段也被拆分成两个阶段,即初始标记
和并发标记
。
CMS完整的收集过程如下:
初始标记(init-mark)
:仅扫描与根节点直接关联的对象并标记,这个阶段必须STW
, 由于跟节点数量有限,所以这个过程非常短暂。
并发标记(concurrent-marking)
:与用户线程并发标记。这个阶段在初始标记的基础上继续向下追溯标记。在并发标记阶段,用户线程和标记线程并发执行,所以用户不会感受到停顿。
**遍历第一个阶段(Init Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象在这个阶段,发生变化的对象标记为Dity**
并发预清理(concurrent-precleaning)
:与用户线程并发进行。在并发标记阶段一些对象的引用已经发生了变化,precleaning
会发现这些引用关系的改变,并将存活的对象标记。举个例子:如果线程A有一个指向对象X的引用,并将该引用传递给了线程B,CMS需要记录下线程B持有了对象X,即使线程A已经不存在了。precleaning
是为了减少下一阶段“重新标记”的工作量,因为remark
阶段会STW
。
将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象
重新标记(remark)
:remark
阶段会STW
。如果应用正在并发运行且在不断地改变对象引用,CMS
则不能准确地确定某个对象是否存活。所以CMS
会在remark
阶段STW
,从而获取所有引用关系的改变。
并发清理(concurrent-sweeping)
:清理垃圾对象,这个阶段GC线程和用户线程并发执行。
并发重置(concurrent-reset)
:重置CMS收集器的数据结构,做好下一次执行GC任务的准备工作。
可以看出,一个存在2次的STW
垃圾回收时promotion failed是个很头痛的问题,一般可能是两种原因产生,第一个原因是救助空间不够,救助空间里的对象还不应该被移动到年老代,但年轻代又有很多对象需要放入救助空间;第二个原因是年老代没有足够的空间接纳来自年轻代的对象;这两种情况都会转向Full GC,网站停顿时间较长。
解决方方案一:
第一个原因我的最终解决办法是去掉救助空间,设置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可,第二个原因我的解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。
解决方案一的改进方案:
又有改进了,上面方法不太好,因为没有用到救助空间,所以年老代容易满,CMS执行会比较频繁。我改善了一下,还是用救助空间,但是把救助空间加大,这样也不会有promotion failed。具体操作上,32位Linux和64位Linux好像不一样,64位系统似乎只要配置MaxTenuringThreshold参数,CMS还是有暂停。为了解决暂停问题和promotion failed问题,最后我设置-XX:SurvivorRatio=1 ,并把MaxTenuringThreshold去掉,这样即没有暂停又不会有promotoin failed,而且更重要的是,年老代和永久代上升非常慢(因为好多对象到不了年老代就被回收了),所以CMS执行频率非常低,好几个小时才执行一次,这样,服务器都不用重启了。
上面介绍了promontion faild产生的原因是EDEN空间不足的情况下将EDEN与From survivor中的存活对象存入To survivor区时,To survivor区的空间不足,再次晋升到old gen区,而old gen区内存也不够的情况下产生了promontion faild从而导致full gc.那可以推断出:eden+from survivor < old gen区剩余内存时,不会出现promontion faild的情况,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 进而推断出:
CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100
例如:
当xmx=128 xmn=36 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913
当xmx=128 xmn=24 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…
当xmx=3000 xmn=600 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33
CMSInitiatingOccupancyFraction低于70% 需要调整xmn或SurvivorRatior值。
原文:https://www.cnblogs.com/hongdada/p/10445686.html