实例透彻分析CMS垃圾收集器执行过程
CMS收集器收集步骤:
- Phase 1:Initial Mark 初始标记
- Phase 2:Concurrent Mark 并发标记
- Phase 3:Concurrent Preclean 并发预清除
- Phase 4:Concurrent Abortable Preclean 并发中止预清除
- Phase 5:Final Remark 最终的重新标记
- Phase 6:Concurrent Sweep 并发清除
- Phase 7:Concurrent Reset 并发重置
Phase 1:Initial Mark
- 这个是CMS两次stop-the-world事件的其中一次,这个阶段的目标是:标记那些直接被GC Roots引用或者被年轻代存活对象所引用的所有对象。
Phase 2:Concurrent Mark
- 在这个阶段Garbage Collector 会遍历老年代,然后标记所有存活的对象,它会根据上一个阶段找到的GC Roots遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用。
- 在上图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化。
- Phase 3:Concurrent Preclean
- 这也是一个并发阶段,与应用的线程并发运行,并不会stop引用的线程。在并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将这个对象的区域(Card)标记为Dirty,这也就是Card Marking
- 在pre-clean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完之后,dirty card标记就会被清除了。
- Phase 4:Concurrent Abortable Preclean
- 这也是一个并发阶段,但是同样不会影响用户的应用线程,这个阶段是为了尽量承担STW(Stop-the-world)中最终标记的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数,完成的工作量或者时钟时间等)
- Phase 5:Final Remark 最终的重新标记
- 这是第二个STW阶段,也是CMS中的最后一个,这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,gc线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW就非常有必要了。
- 通常CMS的Final Remark阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续STW发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些。
- 标记阶段完成
- 经历过这五个阶段之后,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理那些老年代不再使用的对象。
- Phase 6:Concurrent Sweep 并发清除
- 这里不需要STW,它是与用户的应用程序并发运行,这个阶段是:清除那些不再使用的对象,回收它们的占用空间为将来使用。
- Phase 7:Concurrent Reset 并发重置
- 这个阶段也是并发执行的,它会重设CMS内部的数据结构,为下次GC做准备
-
总结
- CMS通过将大量工作分散到并发处理阶段来减少STW时间,在这块做得非常优秀,但是CMS也有一些其他的问题。
- 缺点
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致Full GC的产生,可能引发串行Full GC。
- 空间碎片,导致无法分配大对象。CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
- 对于堆比较大的应用,GC的时间难以预估。
-
实例:
/** -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC */ public class MyTest5 { public static void main(String[] args) { int size = 1024 * 1024; byte[] myAlloc1 = new byte[4 * size]; System.out.println("111111"); byte[] myAlloc2 = new byte[4 * size]; System.out.println("222222"); byte[] myAlloc3 = new byte[4 * size]; System.out.println("333333"); byte[] myAlloc4 = new byte[2 * size]; System.out.println("444444"); } }