一.总体介绍:
CMS是一款优秀的垃圾收集器。众所周知,在oracle公司的Hotspot的架构中,大体上采用分代回收的机制。其中出生代又采用了拷贝复制的方法。如果对象在初生代内存活超过一定次数之后,就可以晋升到老生代中,而CMS垃圾收集器就是专门用来对老生代做收集。随着现代硬件的发展,更多的企业及服务最关注的点在GC的停顿时间,而CMS恰好是一种以获取最短回收停顿时间为目标的收集器,可以给这类服务带来最好的服务效果。
二.算法剖析:
Mostly-Concurrent collection
这个算法是由Boehm等人提出的,并设计了一种基于三色(tricolor)的收集器。它对整个老年代做一个写屏障,所以如果要对老年代做写操作的时候会讲对应的对象置为灰色。它主要分为以下四个阶段:
1).初始标记阶段
暂停所有的其他线程,并记录下直接与root相连的对象。
2).并发标记阶段
同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
3).最终确认标记阶段
将上一阶段做了指针更新的区域和root合并为一个伪root集合,并对其做tracing。从而可以保证真正可达的对象一定被标记了。但同时也会产生一部分被标记为可达,但其实已经是不可达的区域,由于已经没有了到达这个区域的路径,所以并没有办法将它的标志位置为0,则造成了一个暂时的内存泄漏,但这部分空间会在下一次收集阶段被清扫掉。
4).并发清扫阶段
开启用户线程,同时GC线程开始对为标记的区域做清扫。这个过程要注意不要清扫了刚被用户线程分配的对象。一个小trick就是在这个阶段,将所有新分配的对象置为可达的。
下面我们来看一个运用了Mostly-Concurrent collection算法的例子:
a)处于并发标记阶段,经历了初始阶段标记的a。进入并发标记,记录了b, c, d。
b)依旧处于并发标记阶段,b和e发生了指针更新,并且被collector将其对应域置为dirty。
c)处于最终确认标记阶段,对dirty区别做一个重新扫描,并且标记上d。注意此时c已经是垃圾了,但我们无法对它的标志位做更新。
d)处于并发清扫阶段,会将f清扫掉。而c只能等到下一轮的收集,再去回收它咯。
Mostly Concurrent Collection in a Generational System
现代的JVM就是利用CMS收集器对它的老生代做收集。这中间涉及到具体的并发标记的时候,用到Card Table这个概念。因为在Hotspot JVM的世代回收过程中,新生代的空间会比较小,而老生代的空间会比较大。基于老生代空间大,变更小的特点,为了尽量减少GC引起的停顿时间,采用了停顿时间最短的CMS收集器。在CMS的并发标记的过程中,它会将整个老生代的空间切割为一个个block,每个block对应一个card。并且对整个老生代加上write barrier。从而在并发的标记过程中,用card来记录堆内发生写操作的区域。
在具体的实现write barrier时,Hosking和Moss发现操作系统提供的内存保护屏障开销太大了,所以他们重新设计了一个只要2到3条指令就可以实现同样功能的barrier,这样意味着更小的开销,从论文里看这应该是一个轻量级的软件层面的barrier。但这种基于虚拟存储的系统并不能区分对每个block中的基本类型变量做修改还是对其中的引用类型变量做修改,所以会出现很多dirty但并没有做指针更新的block。
由于card还是包含了很多其他信息。所以为了节省更多的空间提高局部性,又提出了Mod Union Table。将每个dirty位单独拉出来做成一个bitmap或者bytemap。从而减少空间开销,提高定位堆中dirty块的速度。
在标记过程中,为了减少collector和mutator之间在读取Object头部而产生的相互干扰,我们并没有将mark bit放到Object的头部。而是将他们抽出来放到外部作为一个单独的数据结构。在具体的实现过程中,对heap内的每4Byte大小做一个对应的bit标记位。通常为了减少GC的停顿时间,我们会将所以root可达的对象放入到这个集合内,称为remember set。但考虑到GC的时候本身就是缺乏资源的,所以这些数据结构不应该占据大量的空间。所以具体的实现过程中只存取了与root直接相邻的对象。
三.CMS的具体过程
初始标记(STW initial mark)
在具体的实现的时候,考虑到GC停顿时间和空间利用开销的因素,我们选择了一个折中的办法。即用了一个外部的bitmap增强局部性尽量减小GC的停顿时间,同时在to-scanned-set内放了直接与root相连的object,并将其标志位置为1,这样可以减小空间的开销。注意这个阶段需要暂停所有的非GC线程。
并发标记(Concurrent marking)
在第二阶段的并发标记的阶段,GC线程会线性的扫描整个堆的bitmap。在线性的遍历bitmap的过程中,如果发现一个标志位为1的object则将它压入to-scanned-set内(具体实现用一个stack来维护)。当stack满的时候,进入循环,将栈内的对象一个个弹出来并扫描其中的引用域。同时遵循如下的规则: (1)引用的对象地址比自己低,注意此时按线性去遍历bitmap已经走过了这个位,则我们要将当前的引用对象压入栈中,并对其标志做一次更新操作置为1。(2)引用对象地址比自己高,则只用将对应的bit标记一下即可,因为根据线性的扫描,之后会扫到他,从而做有关的压栈扫描处理。从而不断的做循环,直到这个栈为空并且扫描结束,注意这个过程发生在并发的阶段,这意味着除了collector在做标记还有mutator在制造垃圾。这种策略的缺陷在于它需要对整个堆的做线性遍历,而不是简单的tracing( tracing是一个图遍历的过程,其算法复杂度为O(N+E) )。
并发预清理(Concurrent Precleaning)
为了尽量减少下一阶段STW的时间。增加了预清理的阶段,主要是对并发标记过程中留下的dirty位做一个tracing。注意这里是并发的执行,所以同时也会对card table和标记表做更新。一般实现采用不多循环遍历dirty表做tracing并对它做clean操作,直到预清理清楚了三分之一以上的dirty表则退出循环(也有的采用的是dirty数小于1000则退出循环)。
重新标记(STW Remark)
暂停所有用户线程,从dirty和root入口做tracing,确保所有可达对象做了标记。
并发清理(Concurrent sweeping)
并发的开启GC和用户线程,其中涉及到对相邻free block的整合问题。因为它可能会由mutator和collector对freeList的操作产生数据的不一致性。所以为了保证一致性,需要对空闲块的操作加上外部锁(exclusion lock)。
并发重置(Concurrent reset)
对标记的表和记录dirty的表做清零。并且重置CMS收集器的其他数据结构,等待下一次垃圾回收。
四.总结
优点:
1).将stop-the-world的时间降到最低,能给电商网站用户带来最好的体验。
2).尽管CMS的GC线程对CPU的占用率会比较高,但在多核的服务器上还是展现了优越的特性,目前也被部署在国内的各大电商网站上。
缺点:
1).对CMS在单核和多核机器上做测试。发现CMS在收集过程中会大量占用CPU的时间。所以在第二个阶段会比较漫长,所以一般将其设置在多核机器上。并且对于CMS在单核机器上的表现设计了一套启发式控制。这种控制将收集器看作一个掠夺者,而收集器会尽量赶在用户线程分配新的对象之前完成收集的工作。同样也有可能会出现用户线程希望分配对象,但目前空间不够,则需要停下收集器,这样会让整个收集时间大大加长。所以这时候一搬会选择扩张堆的大小。
2).Mark Sweep算法一直令人诟病的碎片问题,造成了堆空间的浪费以及利用率的下降。
3).需要较大的内存空间去运行,因为在很多并行的阶段,要考虑到用户程序运行时也要分配空间。所以一般选择在堆利用率达到一个常数的时候就开启CMS的收集。可以在VM argument里来设置这个阀值。(–XX:CMSInitiatingOccupancyFraction =n,n=0~100)
4).会产生浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随程序自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理。
转载自:http://blog.csdn.net/mark__zeng/article/details/48751053