CMS垃圾回收器 & 三色标记算法
Posted 小胖java攻城狮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CMS垃圾回收器 & 三色标记算法相关的知识,希望对你有一定的参考价值。
什么是CMS垃圾回收器?
CMS(Concurrent Mark Sweep)是一款里程碑式的垃圾收集器,为什么这么说呢?因为在它之前,GC线程和用户线程是无法同时工作的,即使是Parallel Scavenge,也不过是GC时开启多个线程并行回收而已,GC的整个过程依然要暂停用户线程,即Stop The World。这带来的后果就是Java程序运行一段时间就会卡顿一会,降低应用的响应速度,这对于运行在服务端的程序是不能被接收的。
1. 执行流程
1、初始标记
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。初始标记的过程是需要触发STW的,不过这个过程非常快,而且初试标记的耗时不会因为堆空间的变大而变慢,是可控的,因此可以忽略这个过程导致的短暂停顿。
2、并发标记
并发标记就是将初始标记的对象进行遍历,以这些对象为根,遍历整个对象图,这个过程耗时较长,而且标记的时间会随着堆空间的变大而变长。不过好在这个过程是不会触发STW的,用户线程仍然可以工作,程序依然可以响应,只是程序的性能会受到一点影响。因为GC线程会占用一定的CPU和系统资源,对处理器比较敏感。CMS默认开启的GC线程数是:(CPU核心数+3)/4,当CPU核心数超过4个时,GC线程会占用不到25%的CPU资源,如果CPU数不足4个,GC线程对程序的影响就会非常大,导致程序的性能大幅降低。
3、重新标记
由于并发标记时,用户线程仍在运行,这意味着并发标记期间,用户线程有可能改变了对象间的引用关系,可能会发生两种情况:一种是原本不能被回收的对象,现在可以被回收了,另一种是原本可以被回收的对象,现在不能被回收了。针对这两种情况,CMS需要暂停用户线程,进行一次重新标记。
4、并发清理
重新标记完成后,就可以并发清理了。这个过程耗时也比较长,且清理的开销会随着堆空间的变大而变大。不过好在这个过程也是不需要STW的,用户线程依然可以正常运行,程序不会卡顿,不过和并发标记一样,清理时GC线程依然要占用一定的CPU和系统资源,会导致程序的性能降低。
2. 什么对象可以称之为GC Roots?
1、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
2、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。
3、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。
4、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。
5、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。
3. CMS的缺点是什么?
1.CMS收集器对CPU资源非常敏感
在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器”的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)
2.CMS处理器无法处理浮动垃圾
CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾”
3、并发失败
由于浮动垃圾的存在,因此CMS必须预留一部分空间来装载这些新产生的垃圾。CMS不能像Serial Old收集器那样,等到Old区填满了再来清理。在JDK5时,CMS会在老年代使用了68%的空间时激活,预留了32%的空间来装载浮动垃圾,这是一个比较偏保守的配置。如果实际引用中,老年代增长的不是太快。
-XX:CMSInitiatingOccupancyFraction
可以通过参数适当调高这个值。到了JDK6,触发的阈值就被提升至92%,只预留了8%的空间来装载浮动垃圾。
如果CMS预留的内存无法容纳浮动垃圾,那么就会导致「并发失败」,这时JVM不得不触发预备方案,启用Serial Old收集器来回收Old区,这时停顿时间就变得更长了。
4.CMS是基于“标记–清除”算法实现的
所以在收集结束的时候会有大量的空间碎片产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。 为了解决这个问题,CMS提供了一个开关参数-XX:CMSFullGCsBeforeCompaction
,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了
4. 三色标记算法
为什么CMS的GC线程可以和用户线程一起工作?是因为有三色标记算法的存在,那什么是三色标记算法呢?
标记流程如下:
- 刚开始,所有的对象都是白色,没有被访问。
- 将GC Roots直接关联的对象置为灰色。
- 遍历灰色对象的所有引用,遍历后灰色对象本身置为黑色,引用置为灰色。
- 重复步骤3,直到没有灰色对象为止。
- 结束时,黑色对象存活,白色对象回收。
5. 为什么GC要STW(stop the world)?
首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。
其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。
这也是三色标记算法存在的问题
漏标是什么?
-
某个状态下,黑色->灰色->白色
-
如果一切顺利,不发生任何引用变化,GC线程顺着灰色的引用向下扫描,最后都变成黑色,都是存活对象
-
但是如果出现了这样一个状况,在扫描到灰色的时候,还没有扫描到这个白色对象,此时,黑色对象引用了这个白色对象,而灰色对象指向了别的对象,或者干脆指向了null,也就是取消了对白色对象的引用
-
那么我们会发现一个问题,根据三色标记规则,GC会认为,黑色对象是本身已经被扫描过,并且它所有指向的引用都已经被扫描过,所以不会再去扫描它有哪些引用指向了哪些对象,然后,灰色对象因为取消了对白色对象的引用,所以后面GC开始扫描所有灰色对象的引用时候,也不会再扫描到白色对象。
最后结果就是,白色对象直到本次标记扫描结束,也是白色,根据三色标记规则,认为它是垃圾,被清理掉
错标是什么?
假设GC线程已经遍历到B,此时用户线程执行了一下以下两个操作
//B到D的引用被切断
B.D=null;
//A到D的引用被建立
A.xx=D;
B到D的引用被切断,且A到D的引用被建立。
此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当做垃圾回收。
可以看到错标的结果比漏表严重的多,浮动垃圾可以下次GC清理,而把不该回收的对象回收掉,将会造成程序运行错误。
漏标和错标存在两个充要条件:
- 有至少一个黑色对象在自己被标记之后指向了这个白色对象
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
这两个条件,必须全满足,才会造成漏标问题。换言之,我们破坏任何一个条件,这个白色对象。就不会再被漏标。
如此一来这样就产生了两个解决办法
CMS采用的是增量更新
增量更新破坏的是第一个条件,我们在这个黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描。
可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰。
这样有一个缺点,就是会重新扫描这个黑色对象的所有引用,比较浪费时间。
G1采用的是原始快照
原始快照破坏的是第二个条件,我们在这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向的白色对象为根,对它的引用进行扫描。
可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰。
这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是被变灰了,就会导致它和它的引用,本来应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾.其实这样是比较可以忍受的,只是让它多存活了一次GC而已,浪费一点点空间,但是会比增量更新更省时间。
public static void main(String[] args)
System.out.println("点个赞再走吧");
以上是关于CMS垃圾回收器 & 三色标记算法的主要内容,如果未能解决你的问题,请参考以下文章