CMS垃圾收集器

CMS收集器

      并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;
      在前面ParNew收集器曾简单介绍过其特点;
1、特点
      针对老年代;
      基于"标记-清除"算法(不进行压缩操作,产生内存碎片);            
      以获取最短回收停顿时间为目标;
      并发收集、低停顿;
      需要更多的内存(看后面的缺点);
            
      是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
      第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
2、应用场景
      与用户交互较多的场景;        
      希望系统停顿时间最短,注重服务的响应速度;
      以给用户带来较好的体验;
      如常见WEB、B/S系统的服务器上的应用;
3、设置参数
      "-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
4、CMS收集器运作过程
      比前面几种收集器更复杂,可以分为4个步骤:
(A)、初始标记(CMS initial mark)
      仅标记一下GC Roots能直接关联到的对象;
      速度很快;
      但需要"Stop The World";
(B)、并发标记(CMS concurrent mark)
      进行GC Roots Tracing的过程;
      刚才产生的集合中标记出存活对象;
      应用程序也在运行;
      并不能保证可以标记出所有的存活对象;
并发标记就需要标记出 GC roots 关联到的对象 的引用对象有哪些。比如说 A -> B (A 引用 B,假设 A 是 GC Roots 关联到的对象),那么这个阶段就是标记出 B 对象, A 对象会在初始标记中标记出来。
这个过程是可以和用户线程并发执行的。所谓的并发的实现,可以有几种方式,比如说,标记了 100 个对象,那么就停一停,让用户线程跑一会;再比如说,标记了 10ms,再停一停,之类的实现。
浮动垃圾问题
如果深入思考,可能会有这样的疑问,假如你标记了一个对象,然后用户线程说,这个对象我不要了,你回收吧。这个时候怎么办?其实这个时候暂时没办法处理,只能留到下一次 GC 的时候再回收,这次 GC 不好意思,GC 不了,这个就叫做浮动垃圾。
这个其实还好,GC 不了对程序不会有影响,大不了多占用了一点内存嘛,反正下次也释放。
但是还可能有这样的问题出现:
假如有一个对象 GC 线程没有标记(用户线程之前没在用),然后轮到了用户线程,用户线程说,这个对象我重新又要用了,不要把这个对象GC 掉,这个时候怎么办?假如这个时候处理不了,还是 GC 了,那么程序就直接报错了,这个是不允许的,解决办法可以如下:
三色标记法
这个算法就是把 GC 中的对象划分成三种情况:
白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
灰色:正在搜索的对象
黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)
上面说的这个问题就是可以用下图来表示:
这个图的意思就是:假设有 A -> B -> C, A 是 GC Roots 关联的对象,那么首先会把 GC Roots 标记,也就是 A 标记成灰色(证明现在正在搜索 A 相关的),然后搜索 A 的引用,也就是 B,那么搜索了 B,把 B 变成了灰色,那么 A 就搜索完成了。(此时注意,现在是不管 C 的,因为 C 不是 A 的引用,现在只管 A 的引用是什么)。此时把 A 相关的搜索完了,那么 A 就变成了黑色,证明 A 已经 ok 了。
(Ps:浮动垃圾就是说,此时 A 又不用了,那么 A 是没办法回收的,因为 A 已经标记了)
此时准备要搜索 B 了。
刚好,此时,用户线程要执行了,用户线程把原来 A -> B -> C 的引用改成了 A -> C,同时 B 不再引用C。
然后又到 GC 线程执行了。
GC 线程发现 B 没有引用的对象了(因为用户线程已经把 B -> C 去掉了),那么 B 就相当于搜索完成了,变成黑色了。
最后,C 怎么办,C还是白色的呢,白色的是不会搜索,当做垃圾处理的。
解决办法
此时的解决办法就是有一个叫做写入屏障的东西。就是说,如果A已经被标记了(已经是黑色的了),那么用户线程改动 A->C的时候,会把 C 变成灰色,这样,以后就可以搜索 C了。
GC 线程和 用户线程并发的时候,用户线程把失效的对象又置为有效,这个时候怎么处理。
就是用重新标记

(C)、重新标记(CMS remark)
      为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
      需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
      采用多线程并行执行来提升效率;
重新标记是干什么的呢?就是由于并发标记这个阶段用户线程和GC 线程并发,假如这个阶段用户线程产生了新的对象,这个对象是白色的,总不能被 GC 掉吧。这个阶段就是为了让这些对象重新标记。
(D)、并发清除(CMS concurrent sweep)
      回收所有的垃圾对象;
      整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;
      所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
      CMS收集器运行示意图如下:

5、CMS收集器3个明显的缺点
                     (A)、对CPU资源非常敏感
      并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
      CMS的默认收集线程数量是=(CPU数量+3)/4;
      当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

      增量式并发收集器:
      针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);
      类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;
      但效果并不理想,JDK1.6后就官方不再提倡用户使用。

(B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
(1)、浮动垃圾(Floating Garbage)
      在并发标记时,用户线程新产生的垃圾,称为浮动垃圾;
所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS无法在当次集中处理它们(为什么?原因在于CMS是以获取最短停顿时间为目标的,自然不可能在一次垃圾处理过程中花费太多时间),只好在下一次GC的时候处理。这部分未处理的垃圾就称为“浮动垃圾”
      这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;
      也要可以认为CMS所需要的空间比其他垃圾收集器大;
      "-XX:CMSInitiatingOccupancyFraction":该值代表老年代堆空间的使用率;比如,value=75意味着第一次CMS垃圾收集会在老年代被占用75%时被触发
      JDK1.5默认值为68%;
      JDK1.6变为大约92%;               
(2)、"Concurrent Mode Failure"失败
      如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;
      这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
      这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。

(C)、产生大量内存碎片
      由于CMS基于"标记-清除"算法,清除后不进行压缩操作;
      前面《Java虚拟机垃圾回收(二) 垃圾回收算法》"标记-清除"算法介绍时曾说过:
      产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
      解决方法:                
(1)、"-XX:+UseCMSCompactAtFullCollection"
      使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
      但合并整理过程无法并发,停顿时间会变长;
      默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)、"-XX:+CMSFullGCsBeforeCompaction"
      设置执行多少次不压缩的Full GC后,来一次压缩整理;
      为减少合并整理过程的停顿时间;
      默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
      由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;
总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;
      但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

上一篇:tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法


下一篇:java.lang.NoClassDefFoundError: net/sf/ezmorph/Morpher