CMS 采用标记清理算法
CMS 采取 垃圾回收线程和系统工作线程尽量同时执行的模式来处理
如何实现?
-
初始标记
这个阶段,系统的工作线程全部停止,进入"stop the world",标记出所有的Gc roots对象。虽然要 stw,但是影响不大,因为速度非常快。
-
并发标记 (Gc roots 深度追踪)
系统线程可以随意创建各种新对象,继续运行,这个阶段可能会创建新的存活对象,也可能部分存活对象失去引用。在这个过程中,垃圾回收线程会尽可能的对已有的对象进行 GC Roots追踪。
简而言之,会对老年代所有对象进行GC Roots 追踪,是最耗时的。被方法区静态变量引用的类其中的实例变量引用的类这一阶段会被标记存活。
-
重新标记
因为第二阶段结束,会多出许多之前没标记的存活对象和垃圾对象,所以再次进入"stop the world",然后重新标记在第二阶段新创建的一些对象和失去引用变成垃圾的对象。这个阶段速度很快。
-
并发清理
系统恢复运行,然后清理之前标记的垃圾对象。很耗时,但是是和程序并发运行,所以不影响系统的运行。
简单来说,为了避免长时间的"Stop the World" ,CMS采用了4个阶段来垃圾回收,其中初始标记和重新标记,耗时短,虽然会导致stw,但是影响不大,然后并发标记和并发清理,两个耗时最长,但是可以跟系统的工作线程并发运行,所以对系统影响不大。
这就是CMS的基本工作原理。
CMS 的弊端:
- 消耗cpu资源
- Concurrent Mode Failure (并发模式失败)
并发清理的时候,系统一直在运行,可能会随着系统的运行让一些对象进入老年代,变成垃圾对象。这种垃圾对象----浮动垃圾。需要等到下一次 Full Gc清理他们。所以为了保证在CMS垃圾回收期间还有一定的空间让一些对象进入老年代,一般会预留一些空间。
“-XX:CMSInitiatingOccupancyFaction” 参数设置老年代占用多少比例时触发CMS垃圾回收。
jdk1.6 默认是 92%
如果在这期间,进入老年代的对象大于可用内存空间,那么会触发 Concurrent Mode Failure,并发垃圾回收失败。这时会自动用"Serial Old" 垃圾回收器替代CMS。就是直接把系统"Stop the World" 重新进行长时间的Gc Roots追踪,标记出全部垃圾对象,不允许新的对象。然后一次性回收,再恢复系统。
- 内存碎片
老年代CMS 采用标记清理算法,标记垃圾,回收,会产生大量的内存碎片。太多的内存碎片会导致频繁的Gc。
“-XX:+UseCMSCompactAtFullCollection” 参数默认打开。
表示在 Full Gc后要再次进行"Stop the world" 停止工作线程,再进行碎片整理,把存活的对象挪到一边,空出大片连续的内存空间。
“-XX:CMSFullGCsBeforeCompaction”
执行多少次 Full Gc 之后再执行内存碎片整理工作。默认是 0,每次都需要。
为什么老年代的 Full GC 比新生代的 Minor GC 慢很多倍?一般在10倍以上?
ParNew的Minor GC
新生代执行速度快,因为直接从 Gc Roots 出发,追踪哪些对象是存活的就行,新生代存活的对象是很少的。然后直接放入Survivor,就一次性回收eden区和之前的Survivor区。
CMS的Full GC
-
在并发标记阶段,需要去深度追踪所有的存活对象,老年代存活对象很多。
-
并发清理阶段,也不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象。
-
最后还要执行内存碎片整理,把存活对象移一起,空出连续空间,这个过程还得 “Stop the world”
-
并发清理期间,剩余空间不足存放新进的对象时,还会触发"Concureent Mode Failure",更加麻烦,还要使用"Serial Old"单线程的垃圾回收器,"Stop the world"后再重新来一遍回收过程。更加耗时。