初步了解

  • 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。所以CMS需要设当的进行内存整理,而不是轻易的进行Full GC,可以搭配两个参数进行优化。

  • -XX:+UseCMSCompactAtFullCollection(默认是开启的,JDK9后删除)
  • -XX:CMSFullGCsBeforeCompaction=n(JDK9后删除)

详细介绍可以参考下面的参数讲解。

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(默认是开启的,JDK9后删除)

    用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于内存整理必须移动存活的对象,(在Shenandoah和ZGC出现之前)这个操作是无法并发的,所以又会使停顿时间变长,从而解决内存碎片的问题。为了减少内存整理的停顿时间,可以配合下面这个参数来进行控制内存整理触发频率。

  • -XX:CMSFullGCsBeforeCompaction=n(JDK9后删除)
    设置在执行了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阶段停顿时间过长的问题。

参考文章