老年代垃圾回收器CMS是如何工作的

CMS回收器使用的垃圾回收算法是标记-清理,就是去找到那些没有GC Roots直接或间接引用的对象,对他们进行多次标记,最后回收掉,标记-清理算法会产生大量的内存碎片

什么时候触发老年代GC?

  • MinorGC前,如果当前老年代的剩余空间小于新生代的对象总大小,并且没有设置空间分配担保JVM参数,直接触发Full GC

  • 在空间分配担保机制进行检查时,如果当前老年代剩余空间小于之前平均每次MinorGC后进入老年代的对象总大小时,触发Full GC

  • 当Minor GC后,Survivor区中无法存放剩余存活对象,并且此时老年代剩余空间也不足以存放这些对象时,触发Full GC

  • 老年代存活对象空间占比达到了JVM参数“-XX:CMSInitiatingOccupancyFaction设置的阈值(默认为92%),触发Full GC

CMS回收器进行垃圾回收的步骤

分为四步

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清理

其中,初始标记和重新标记阶段会出现STW,因为STW会让系统停止对外服务,如果此时标记时间过长,会严重影响系统性能,因此CMS回收器使用垃圾回收线程和系统工作线程尽量同时执行的模式来处理的

初始标记

CMS进行第一次标记,触发STW,这个阶段的速度比较快,因为这次标记只标记那些被GC Roots直接引用的对象

GC Roots一般有局部变量,静态变量等,不包含实例变量

并发标记

此时,工作线程和GC线程是同时工作的,系统可以对外提供服务

该阶段GC线程会尽可能对已有的对象进行GC Roots追踪,包括那些被间接引用的对象

这个阶段非常耗时,但因为是和系统工作线程并发的,所以不会影响系统性能

重新标记

这个阶段也会触发STW,由于在并发标记期间,系统还在不断的创建新的对象,因此内存中还会有许多存活对象和垃圾对象是没有被标记出来的

此时会重新标记那些存活的对象

这里标记的速度也非常快,因为只需要对少数的对象进行重新标记即可

并发清理

该阶段就是回收掉垃圾对象,比较耗时,但是因为没有STW,所以不会对系统性能造成影响

为什么老年代的垃圾回收特别慢?

  • CMS执行GC时,会有两个并发阶段,此时工作线程和GC线程同时工作,GC线程不能充分利用CPU资源,CMS默认设置的GC线程数量是(CPU核数 + 3) / 4

  • 在进行并发清理时,会产生不少浮动垃圾(工作线程会让一些对象进入老年代,同时这些对象没多久就成为垃圾对象了),但此时CMS无法清理这些对象,如果在这个期间,新生代的存活对象要进入老年代并且此时老年代剩余空间不足,还会触发concurrent mode failure,此时系统会自动启动serialOld回收器,触发STW,同时再执行一次标记清理的过程

  • Full GC不仅仅采用了标记清理算法,因为标记清理算法会产生大量的内存碎片,这样会导致老年代中连续可用的空间变少,增加触发Full GC的概率,因此CMS有一个默认的JVM参数-XX:+UseCMSCompactAtFullCollection,在每次Full GC后要触发STW,来整理这些内存碎片,通过JVM参数-XX:CMSFullGCsBeforeCompaction可以设置执行多少次Full GC后进行内存整理,默认为0,即每次Full GC后都进行整理

  • 老年代进行标记时,不同与新生代,由于大部分对象都是存活的,所以消耗的时间比较长