CMS收集器
我们平时在写代码的时候,相信大部分同学几乎都没有考虑过垃圾回收啥的吧?就是不停的疯狂写代码,然后直接上线部署,也不考虑我们写的代码对内存是否有影响,对垃圾回收是否有影响。然后当系统运行起来一段时间后,就发现各种卡顿,频繁触发Full GC。
类似的情况有很多,我们不能过于理想化的期待永远没有Full GC,还是要针对老年代的垃圾回收器的工作原理做到心理有数,从而更好的做调优工作。
一般老年代我们选择的垃圾回收器就是CMS(Concurrent Mark Sweep) 收集器 ,这是一种以获取最短回收停顿时间为目标的收集器。 我们大部分互联网网站或者基于浏览器的B/S系统的服务端 ,这类应用通常都会较为关注服务的响应速度, 希望系统停顿时间尽可能短, 以给用户带来良好的交互体验。 CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”) 上就可以看出CMS收集器是基于标记-清除算法实现的, 它的运作
过程相对于前面几种收集器来说要更复杂一些, 整个过程分为四个步骤, 包括:
1) 初始标记(CMS initial mark)
2) 并发标记(CMS concurrent mark)
3) 重新标记(CMS remark)
4) 并发清除(CMS concurrent sweep)
接下来我们来一步一步分析整个CMS的运行逻辑和流程,尽量把它的核心思路掌握。
1) 初始标记(CMS initial mark)
首先根据之前讲过的“可达性分析算法“来判断有哪些对象是被GC Roots给引用的,如果是的话就是存活对象,否则就是垃圾对象。然后将垃圾对象都标记出来,如下图:
注意:初始标记的过程会让系统停止工作,进入“Stop The World”状态,不过这个过程很快,仅仅标记GCRoots直接引用的那些对象。(回顾下GC Roots对象有:类的静态变量,方法的局部变量,但是类的实例变量不是GCRoots)
假设我现在系统中有这样一段代码:
public class Test{
private static Company company = new Company();
}
public class Company{
private Employee employee = new Employee();
}
那么在内存中对应的初始标记阶段只会标记出来GC Roots直接引用的对象,也就是Company()对象,而employee对象仅仅是类的实例变量,不会被进行标记。内存图如下:
注意:Employee对象仅仅是类的实例变量引用的对象,不是GCRoot直接引用的对象,因此初始标记并不会进行标记。
2)并发标记(CMS concurrent mark)
并发标记阶段恢复系统正常运行,可以随意创建对象,同时并发标记线程也开始工作,这里由于一边进行并发标记,一边进行对象的创建,必然会持续增加新的对象产生,同时也有可能一些对象失去引用变成垃圾对象。
那么并发标记主要是标记哪些对象呢?比如上图中的Employee对象,垃圾回收线程会判断该对象被谁引用,这里是被company对象引用,再次判断company对象被谁引用,由于初始标记的时候已经知道是被GCRoots直接引用,从而判断到Employee对象是间接被GCRoots对象引用,从而标记为存活对象。
总之,针对所有老年代中存在的对象以及不断新增的对象都会进行标记,而我们的系统线程也在一直工作不断产生对象,所以该阶段也是最耗时的。虽然是耗时的,但是垃圾回收与系统是并行进行的,所以并不会对系统的运行造成影响。
3)重新标记(CMS remark)
由于我们的第二个阶段是并发标记,那么肯定会造成有部分对象已经失去引用变成垃圾对象没有来得及更正,以及新创建的对象还未来得及标记,如下图:
因此第三阶段:重新标记 会暂停我们的系统线程,开始重新整理,如下图:
不过该阶段会很快,主要是针对第二阶段中被系统程序运行变动过的少数对象进行标记,所以速度很快。
接着重新恢复系统线程工作,开始进入第四阶段:并发清理。
4)并发清除(CMS concurrent sweep)
最后是并发清除阶段, 清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的。
5)小结
通过以上CMS工作的整个过程,我们总结下:
- 最耗时的阶段是:并发标记与并发清除--->不过该阶段是与用户线程并发执行并不影响系统
- 初始标记和重新标记阶段:需要Stop the World,暂停系统工作---->但是该两个阶段速度很快几乎影响不大
通过一张完整的流程图来表示我们CMS的工作逻辑: