CMS是一款优秀的垃圾收集器。众所周知,在Oracle公司的Hotspot的架构中,大体上采用分代回收的机制。其中新生代又采用了拷贝复制的方法。如果对象在新生代内存活超过一定次数之后,就可以晋升到老生代中,而CMS垃圾收集器就是专门用来对老生代做收集。随着现代硬件的发展,更多的企业及服务最关注的点在GC的停顿时间,而CMS恰好是一种以获取最短回收停顿时间为目标的收集器,可以给这类服务带来最好的服务效果。
CMS是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Parallel New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。可以通过JVM启动参数:-XX:+UseConcMarkSweepGC来开启CMS。
CMS只会回收老年代和永久带(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻带;CMS是一种预处理垃圾回收器,它不能等到老年代内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以CMS垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%;
CMS收集过程
CMS 处理过程有七个步骤:
1.初始标记(CMS-initial-mark) ,会导致stop the world;2.并发标记(CMS-concurrent-mark),与用户线程同时运行;3.预清理(CMS-concurrent-preclean),与用户线程同时运行;4.可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;5.重新标记(CMS-remark) ,会导致stop the world;6.并发清除(CMS-concurrent-sweep),与用户线程同时运行;7.并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
其运行流程图如下所示:
初始标记(CMS-initial-mark)
这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:
1.标记老年代中所有的GC Roots指向的对象,如下图节点1;2.标记年轻代中活着的对象引用到的老年代的对象(指的是年轻代中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;
在Java语言里,可作为GC Roots对象的包括如下几种:
1.虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;2.方法区中的类静态属性引用的对象 ;3.方法区中的常量引用的对象 ;4.本地方法栈中JNI的引用的对象;
ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数。
并发标记(CMS-concurrent-mark)
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
由于这个阶段是和用户线程并发的,可能会导致concurrent mode failure。
并发预清理(CMS-concurrent-preclean)
为了尽量减少下一阶段STW的时间。增加了预清理的阶段,主要是对并发标记过程中留下的dirty位做一个tracing。注意这里是并发的执行,所以同时也会对card table和标记表做更新。一般实现采用不多循环遍历dirty表做tracing并对它做clean操作,直到预清理清楚了三分之一以上的dirty表则退出循环(也有的采用的是dirty数小于1000则退出循环)。
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Dirty的Card 如下图所示,在并发标记阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;
最后将6标记为存活,如下图所示:
可被终止的预清理(CMS-concurrent-abortable-preclean)
这个阶段尝试着去承担下一个阶段重新标记阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次young gc,清理年轻代的引用,使得下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;
重新标记(CMS-remark)
这个阶段会导致第二次stop the word,该阶段的任务是标记整个年老代的所有的存活对象。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,重新标记阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量在年轻代是足够干净的时候运行重新标记阶段。
这个阶段,重新标记的内存范围是整个堆,包含新生代和老年代。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代;因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次young gc,回收掉年轻代的对象无用的对象,并将对象放入幸存代或晋升到老年代,这样再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled。
并发清理(CMS-concurrent-sweep)
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。这个阶段主要是清除那些没有标记的对象并且回收空间;由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
并发重置(CMS-concurrent-reset)
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!