初步了解

  • CMS GC是HotSpot虚拟机第一款真正意义上的并发收集器,第一次实现了垃圾收集线程与用户线程同时工作
  • CMS GC的特点是尽可能减少STW时间,即低延迟,Parallel是吞吐量优先
  • CMS采用标记-清除算法Stop The World机制,在空间碎片化严重的时候也会使用一次标记-压缩算法来整理空间

工作流程

CMS的工作流程大致分为七个步骤。

  1. 初始标记阶段

  2. 并发标记阶段

  3. 预清理阶段

  4. 可被终止的预清理阶段

  5. 重新标记阶段

  6. 并发清除阶段

  7. 并发重置

初始标记阶段

这个阶段主要的工作就是标记出GC Roots能直接关联的对象,这里可分为两部分。

  • 标记老年代所有的GC Roots对象
  • 标记年轻代中活着的对象引用到老年代的对象

这个阶段因为涉及到了GC Roots的确定,所以会发生STW,为了减短STW时间,这个阶段的标记只是初始标记,之后还会继续标记。

并发标记阶段

由初始标记阶段标记到的对象开始寻找所有存活的对象,这个阶段是并发执行的,用户线程与标记线程同时进行,不会发生STW。

因为此时是用户线程和标记是并发执行的,所以有可能会出现一些情况,会导致有些对象会漏标。比如

  1. 年轻代的对象变成老年代对象
  2. 直接在老年代分配对象
  3. 老年代的对象进行引用的更新

除了这些常见的情况,还会有一些特殊情况,所以这个阶段的标记是不完整的,还需要后面标记。

为了提高后面的重新标记的效率,该阶段还会把上述漏标的对象所在的Card标识为Dirty,后续重新标记的时候就只需要查看这些标记为Dirty的对象。

这个阶段会发生Concurrent Mode Failure。之后还会讲到。

预清理阶段

这个阶段就是用来处理上个阶段标识为Dirty的对象,也是并发发生的,并找到引用标识为Dirty的对象的存活对象进行标记,最后清除Dirty标识。

可终止的预处理阶段

这个阶段相当于提前做一些下个阶段的工作,尽可能的减少STW,还会处理一些Dirty的对象,这个阶段是并发的。

这个阶段默认持续的时间是5秒,即时间超过5秒,就会终止这个阶段,或者当eden区使用内存值小于CMSScheduleRemarkEdenPenetration,默认50%时,也会退出这个阶段。

可终止的预处理阶段就是为了减少重新标记阶段的时间,当在可终止的预处理阶段时碰上了yound GC的时候,就会大大减少下个阶段的STW,因为重新标记阶段是扫描整个堆的,所以碰上了yound GC就会减少扫描年轻代的压力。

重新标记阶段

这个阶段是CMS最后的标记阶段,所以这个阶段的主要任务就是标记所有老年代的存活对象。

这个阶段的主要任务如下:

  1. 扫描GC Roots的引用链,确定老年代存活对象
  2. 扫描整个年轻代,查找是否有年轻代对象引用了老年代
  3. 处理所有之前遗留下来的Dirty的对象

因为这个阶段需要确定所有老年代的存活对象,所以不能在并发处理了,不然又会有其他引用变化导致标记不全,所以这个阶段是会发生STW的,而且这个停顿时间是整个CMS垃圾回收里时间最长的一个。

并发清除阶段

清除那些“死了”的对象,因为这个阶段是标记-清除算法中的清除阶段,不涉及移动对象,所以是并发执行的。

因为这个阶段是并发的,有可能也会产生垃圾,但是这次GC的标记阶段已经过去了,所以就只能下次GC的时候清理了。

因为CMS是标记-清除算法,所以这个阶段也会产生一些内存碎片。

并发重置阶段

这个阶段会重置CMS的一些数据结构,为下次GC做准备。

注意点

CMS不是Full GC

虽然CMS有扫描到年轻代,但是CMS只会处理老年代的死亡对象,扫描年轻代也只是为了确定一些老年代的对象是否被引用,所以注意和Full GC进行区分。

CMS回收过程会发生STW

尽管CMS是并发回收,但是也涉及到可达性分析算法,所以一定会发生STW,只是CMS将部分标记放在了并行的阶段上,尽量减短了STW的时间。

CMS发生STW的阶段只有两个,初始标记阶段和重新标记阶段,主要的停顿时间是重新标记阶段,因为这里是要确定老年代所有的存活对象,扫描了整个堆。

CMS尽量做到低延迟,但因此丢弃了部分吞吐量,适合用在一些服务器上。

内存碎片

因为CMS采用的是标记-清除算法,所以清除的过程中就会出现内存碎片的问题,内存碎片严重时候还会导致提前触发Full GC。

Concurrent Mode Failure

因为CMS是并发回收的,所以用户线程也是会有可能在GC的过程中消耗内存,年轻代会在一些情况下就会将部分年轻代存活的对象放入老年代,而此时老年代没有足够的空间,此时就会出现Concurrent Mode Failure

为了解决整个问题CMS一般都会设置一个阈值,当堆内存使用率到达一定阈值的时候,就会开始初始标记阶段,而不是堆内存快不够用的时候发生GC。将GC的时间提前,尽量减少Concurrent Mode Failure的出现。

此时的虚拟机就会触发STW启动Serial Old GC来重新进行老年代的垃圾回收,这样停顿时间会更长。这时候Serial Old GC也会因此进行一次碎片整理,解决标记-清除出现的内存碎片化严重的情况。

Promotion Failed

在进行Minor GC的时候,Survivor空间不足,只能将年轻代的一些对象放入老年代,此时老年代没有足够内存空间存放这些对象,又或者是内存碎片严重,找不到足够的连续的空间存放对象,与Concurrent Mode Failure是有点类似的。

CMS的参数

  • -XX:UseConcMarkSweepGC
    指定CMS为老年代的垃圾收集器,开启这个参数设置之后,就会使用ParNew为新生代垃圾回收器,CMS为老年代的垃圾回收器,Serial Old为后备回收器的组合方案。

  • -XX:CMSInitiatingOccupancyFraction=n

    使用这个参数表示,当老年代的内存使用达到n%的时候就开始回收。JDK5以前的版本默认n是68,JDK6之后默认是92。

    在内存使用增长慢的时候,可以设置大一点的阈值,这样可以减少GC的触发频率,如果增长快,可以设置低一点,避免频繁使用老年代串行收集器。

    这个参数设置的好,则可以减少Full GC的触发频率,有效解决Concurrent Mode Failure的问题。

  • -XX:+UseCMSCompactAtFullCollection

    用于指定执行完Full GC后对内存空间进行压缩整理,避免内存碎片严重的情况,但是这个过程是无法并发的,所以就会使停顿时间变长。

  • -XX:CMSFullGCsBeforeCompaction=n
    设置在执行了n次Full GC之后,就会对内存进行压缩整理,和XX:+UseCMSCompactAtFullCollection参数搭配使用。

    XX:CMSFullGCsBeforeCompaction=n和XX:+UseCMSCompactAtFullCollection参数搭配使用l可以有效解决内存碎片的问题。

  • -XX:ParallelGCThreads=n
    这个参数是设置ParallelGC的线程数的,CMS默认的启动线程数是(ParallelGCThread+3)/4

  • -XX:+CMSScavengeBeforeRemark

    在执行remark操作之前先做一次young GC,因为重新标记阶段是会扫描整个堆的,young GC会帮助扫描年轻代,减少重新标记的STW时间。

    这个参数可以解决remark阶段停顿时间过长的问题。

参考文章