CMS垃圾收集器介绍

原创 2015年09月26日 13:20:09

为期两个月的阿里JVM实习结束了。

在离开科大去实习之前准备了一篇关于Java5中提出的Concurrent Mark Sweep收集器的介绍。

现在贴出来:

CMS垃圾收集器
                                                            中国科学技术大学 软件学院 曾鸣堃
一.总体介绍:
       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去处理。
 


版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

[解题报告]376. Wiggle Subsequence

Problem Background: A sequence of numbers is called a wiggle sequence if the differences between s...

OS里关于函数调用机制

堆栈是C运行时时必须的一个调用路径和参数的空间 ——函数调用框架 ——传递参数 ——保存返回地址 ——提供局部变量空间 esp ebp 每个函数都有自己的esp、ebp。 所以递归嵌套的就会产生很...

存储器层次结构基础(一)

最近在看David Patterson的量化研究方法,顺便记录一下自己的理解。 现代的计算机中的存储器实际是分为很多层次的,从上往下容量不断增大,成本降低,访问速度下降。 Register->Cach...

Hackerrank && Network

1.Hackerrank 一个在N个数字里,选K个数字,使得

当函数返回对象的引用?

当函数返回对象的引用?  // prefix : increment and then fetch  INT& operator++()  {    ++(this->m_i);    retur...

test

看谁最先发现我的这个blog

JVM体系结构-----深入理解内存结构

一、概述        内存在计算机中占据着至关重要的地位,任何运行时的程序或者数据都需要依靠内存作为存储介质,否则程序将无法正常运行。与C和C++相比,使用Java语言编写的程序并不需要显示的为每...
  • ljheee
  • ljheee
  • 2016-08-17 00:42
  • 1795

深入理解java虚拟机(一):java内存区域(内存结构划分)

图一:java内存结构划分 由上图可知,java内存主要分为6部分,分别是程序计数器,虚拟机栈,本地方法栈,堆,方法区和直接内存,下面将逐一详细描述。 1、程序计数器 线程私有,即每个线程都会有一个...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)