CMS 收集器

Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间(STW 短),和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制整个过程分为以下 4 个阶段:

初始标记

只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
比如下图代码
notion image
在初始标记阶段,仅仅会通过replicaManager这个类的静态变量代表的GC Roots,去标记出来他直接引用的ReplicaManager对象,这就是初始标记的过程。
他不会去管ReplicaFetcher这种对象,因为ReplicaFetcher对象是被ReplicaManager类的“replicaFetcher”实例变量引用的 之前说过,方法的局部变量和类的静态变量是GC Roots。但是类的实例变量不是GC Roots。
 

并发标记

进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程
 
在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪。
所谓进行GC Roots追踪,意思就是对上图类似ReplicaFetcher之类的全部老年代里的对象,他会去看他被谁引用了?
比如这里是被ReplicaManager对象的实例变量引用了,接着会看,ReplicaManager对象被谁引用了?会发现被Kafka类的静态变量引用了。
那么此时可以认定“ReplicaFetcher”对象是被GC Roots间接引用的,所以此时就不需要回收他
notion image
第二个阶段,就是对老年代所有对象进行GC Roots追踪,其实是最耗时的他需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。

重新标记

为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程.
这个重新标记的阶段,是速度很快的,他其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。

并发清除

清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行
 
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行
 

CMS收集器缺陷

并发回收垃圾导致CPU资源紧张

CMS垃圾回收器有一个最大的问题,虽然能在垃圾回收的同时让系统同时工作,在并发标记和并发清理两个最耗时的阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分。
并发标记的时候,需要对GC Roots进行深度追踪,看所有对象里面到底有多少人是存活的但是因为老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。并发清理,又需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时的。
所以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4。
我们用最普通的2核4G机器和4核8G机器来计算一下,假设是2核CPU,本来CPU资源就有限,结果此时CMS还会有个(2 + 3) / 4= 1个垃圾回收线程,去占用宝贵的一个CPU。
所以其实CMS这个并发垃圾回收的机制,第一个问题就是会消耗CPU资源。

Concurrent Mode Failure

在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象
但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是浮动垃圾
notion image
如上图所示
在并发清理期间,系统程序可能先把某些对象分配在新生代,然后可能触发了一次MinorGC,一些对象进入了老年代,然后短时间内又没人引用这些对象了。
这种对象,就是老年代的“浮动垃圾”。
因为他虽然成为了垃圾,但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们
所以为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。
CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。
-XX:CMSInitiatingOccupancyFaction参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。
 
也就是说,老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。
那么如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,此时会如何?
这个时候,会发生Concurrent Mode Failure,就是说并发垃圾回收失败了,我一边回收,你一边把对象放入老年代,内存都不够了。
此时就会自动用Serial Old垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
所以在生产实践中,这个自动触发CMS垃圾回收的比例需要合理优化一下,避免“Concurrent Mode Failure”问题

内存碎片问题

老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生。
如果内存碎片太多,会导致后续对象进入老年代找不到可用的连续内存空间了,然后触发Full GC。
所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC。
CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了他意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。
还有一个参数-XX:CMSFullGCsBeforeCompaction,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。