18、JVM 调优实战 - JVM老年代垃圾回收器CMS的内部工作机制

1. CMS垃圾回收的基本原理

一般老年代选择的垃圾回收器是CMS,采用的是标记清理算法。采用标记方法去标记出哪些对象是垃圾对象,然后就把这些垃圾对象清理掉。

 

上图,是一个老年代内存区域的对象分布情况,假设因为老年代内存空间小于了历次Minor GC后升入老年代对象的平均大小,判断Minor GC有风险,可能就会提前触发Full GC回收老年代的垃圾对象。

或者是一次Minor GC的对象太多了,都要升入老年代,发现空间不足,触发了一次老年代的Full GC。

也就是执行一次标记-清理算法。该算法,先通过追踪GC Roots 的方法,看看各个对象是否被GC Roots 给引用了,如果是的话,那就是存活对象,否则就是垃圾对象。

先将垃圾对象都标记出来,然后一次性把垃圾对象都回收掉,如下图:

 

这种方法有问题,他会造成很多内存碎片。而这些内存碎片可能因为太小放不下任何对象,所以会导致这部分内存被浪费了。

这就是CMS采取的 “标记-清理” 算法

2. 如果Stop the World然后垃圾回收会如何?

如果停止一切工作线程,然后慢慢的去执行 “标记-清理” 算法,会导致系统卡死时间过长,很多响应无法处理。

所以CMS垃圾回收器采取的是 垃圾回收线程和系统工作线程尽量同时执行的模式来处理的

3. CMS 如何实现系统一边工作的同时进行垃圾回收?

CMS在执行一次垃圾回收的过程一共分为4个阶段:

1、 初始标记;
2、 并发标记;
3、 重新标记;
4、 并发清理;

首先,CMS要进行垃圾回收时,会先执行初始标记阶段,这个阶段会让系统的工作线程全部停止,进入 “Stop the World” 状态,如下图:

 

1、 初始标记

所谓的“初始标记” ,就是说标记出所有GC Roots直接引用的对象。

 

如上述代码,在初始标记阶段,仅仅会通过“replicaManager” 这个类的静态变量代表的 GC Roots,去标记出来它直接引用的 ReplicaManager 对象,这就是初始标记的过程。

它不会去管ReplicaFetcher这种对象,因为ReplicaFetcher对象是被 ReplicaManager类的 “replicaFetcher” 实例变量引用的。

方法的局部变量和类的静态变量是 GC Roots。但是类的实例变量不是 GC Roots。

 

所以第一阶段,初始标记,虽然说要造成 “Stop the World” 暂停一切工作线程,但是其实影响不大,因为他的速度很快,仅仅标记GC Roots直接引用的那些对象罢了。

2、 并发标记

第二个阶段,是并发标记,这个阶段会让系统线程可以随意创建各种新对象,继续运行。

在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪。

所谓进行GC Roots追踪,意思就是对类似 “ReplicaFetcher” 之类的全部老年代里的对象,他会去看他被谁引用了?

比如这里是被 “ReplicaManager” 对象的实例对象引用了,接着会看, “ReplicaManager” 对象被谁引用了?会发现被 “Kafka” 类的静态变量引用了。

那么此时可以认定 “ReplicaFetcher” 对象是被 GC Roots 间接引用的,所以此时就不需要回收它。如下图:

 

在这个过程中,在进行并发标记的时候,系统程序会不停的工作,它可能会各种创建出来新的对象,部分对象可能成为垃圾,如下图:

 

总结,第二阶段,就是对老年代所有对象进行GC Roots追踪,且是最耗时的。它需要追踪所有对象是否需从根源上被GC Roots 引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。

3、 重新标记阶段

因为第二阶段里,一边标记存活对象和垃圾对象,一边系统在不停运行创建新对象,让老对象编程垃圾。

所以第二阶段结束之后,绝对会有很多存活对象和垃圾对象,是之前第二阶段没标记出来的,如下图:

 

此时进入第三阶段,要继续让系统程序停下来,再次进入“Stop the World” 阶段。

然后重新标记下在第二阶段里新建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况,如下图:

 

这个重新标记的阶段,速度很快。它是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。

4、 并发清理

接着重新恢复系统程序的运行,进入第四阶段:并发清理

当前阶段是让系统程序随意运行,然后他来清理掉之前标记为垃圾的对象即可。

这个阶段是很耗时的,因为需要进行对象的清理,但是它也是跟系统程序并发运行的,所以不影响系统程序的执行。如下图:

 

4. 对CMS的垃圾回收机制进行性能分析

在CMS的垃圾回收机制中,它已经尽可能的进行了性能优化了。

在对老年代全部对象进行GC Roots追踪,标记出来到底哪些可以回收,然后就是对各种垃圾对象从内存里清理掉,这是最耗时的。

它的第二阶段和第四阶段,都是和系统程序并发执行的,所以基本这两个最耗时的阶段对性能影响不大。

只有第一个阶段和第三个阶段是需要 “Stop the World” 的,但是这两个阶段都是简单的标记而已,速度非常的快,所以基本上对系统运行响应也不大。

5. 思考题

为什么老年代的垃圾回收速度会比新生代的垃圾回收速度慢很多倍?到底慢在哪里?

答:

1、 GC Roots搜索所有可达的对象,老年代存活对象更多回收更耗时;

2、 ParNewGC因为其独占性回收性能高于CMS GC;

3、 年轻代使用复制算法采用空间换时间的一种优化

6. 总结

因为老年代的回收算法经历了4个过程更加复杂,分别要经历:

1、 初始标记GC Roots 直接引用的对象;

2、 并发标记,对存量全部对象从根源上判断是否存在GC Roots非常耗时;

3、 重新标记对第二阶段新产生少量对象重新标记追踪;

4、 并发清除,把标记的垃圾对象全部清除,然后还要对存活的对象进行压缩,避免内存碎片化

所以非常耗时。

年轻代的垃圾回收算法:

足够简单高效,通过可达性分析算法标记存活对象,然后把存活对象直接转移至其中的一个Survivor区,然后对另外一块Survivor区和Eden区直接清空即可。

这里做个比喻,我们的磁盘文件里面有非常多的文件,我现在只想保留少数我还需要的文件,最快的办法就是找出那些我还需要的,拷贝出来,然后对磁盘进行格式化。