弄浪的鱼

为什么老年代的 Full GC 要比新生代的 Minor GC 慢很多倍,一般在 10 倍以上?我们可以从了解老年代常用的垃圾回收器 CMS 的工作原理开始回答这个问题。

与新生代采用的复制算法不同,CMS 采用的垃圾回收算法是标记整理算法。且老年代的垃圾回收更加复杂,总共分成 4 个阶段,它们分别是:初始标记、并发标记、重新标记、并发清理。

我们从 CMS 的垃圾回收基本原理开始,了解 CMS 是如何工作的。

CMS 垃圾回收的基本工作原理

老年代常用的垃圾回收器是 CMS,CMS 采用的垃圾回收算法是标记整理算法。描述起来也不难,就是在 Old GC 的时候,CMS 会追踪老年代中对象是否被 GC Root 引用,没有被 GC Roots 引用的对象就会被标记为垃圾对象,然后被清理掉。

标记整理

总的来说,老年代会在两种情况下触发 Old GC:一是开启分配担保机制,根据历次 Minor GC 后进入老年代的对象大于当前老年代内存大小,判断 Minor GC 有风险,则会触发 Old GC;二是 Minor GC 后剩余对象太多,老年代放不下了也会触发 Old GC。

此时老年代的 CMS 垃圾回收器就会用上图所示的标记整理算法回收那些垃圾对象,当然回收的过程并不是这样一步到位的,而是经过了 4 个阶段。

为什么 CMS 垃圾回收过程分成 4 个阶段?

新生代使用的垃圾回收算法一般是复制算法。当新生代满了之后,JVM 会 「Stop the world」 并一次性完成对新生代对象的垃圾回收。那么 CMS 怎么不 「Stop the world」,再一次性完成垃圾回收呢?

新生代的对象最终存活的很少,80% 以上都是垃圾对象,即使工作线程停止工作也只会卡顿一两秒。老年代中存活的对象多,标记和整理的过程会导致系统长时间卡顿,在此期间无法处理系统请求。

所以 CMS 分成初始标记、并发标记、重新标记、并发清理这 4 个阶段,其中最好是的并发标记和并发清理阶段,采用 JVM 线程和工作线程同时运行的方式,在垃圾回收的同时不影响系统的运行。

初始标记阶段

CMS 进行垃圾回收的时候首先会进入初始标记阶段,这一阶段会「Stop the world」,标记出被 GC Roots 直接引用的对象。

这里所说的被 GC Roots 直接引用的对象是怎么样的对象呢?我们看下面这样一段代码:

1
2
3
4
5
6
7
public class Order {
public static User user = new User();
}

public class User {
private Position position = new Position();
}

首先我们应该 GC Roots 是类的静态变量,或者方法的局部变量。在这里我们很容易看出,user 属于类的静态变量,而 User 对象是被 user 直接引用的对象,所以 User 是被 GC Roots 直接引用的对象。

position 是类的实例变量,它既不是类的静态变量也不是方法的局部变量,它不是 GC Roots,position 引用的 Position 对象就不是被 GC Roots 直接引用的对象。

初始标记

初始标记的过程如上图所示,被 GC Roots 直接引用的 User 对象将会被标记出来,被间接引用的 Position 对象还未被标记。

并发标记阶段

初始标记阶段会「Stop the world」,暂停一切工作线程,但是对系统影响不大,因为初始标记阶段只会标记被 GC Roots 直接引用的对象,标记的过程是很快的。

并发标记阶段工作线程和垃圾回收线程将会同时工作,并尽可能对已有对象进行 GC Roots 追踪。同时工作即垃圾回收线程工作的同时,工作线程正常运行,会创建新的对象,也会让一些对象失去引用。

GC Roots 追踪指的就是对 Position 这样的对象进行进一步的追踪,会发现它被 User 的实例变量 postion 引用了,而 position 则被 Order 对象的静态变量 user 引用了。在并发标记阶段,Position 对象就会被标记为被 GC Roots 间接引用,就不会被回收。

并发标记

并发标记阶段要 GC Roots 追踪老年代中的所有对象,是最耗时的,不过此时垃圾回收线程与工作线程同时工作,不会对系统正常运行造成影响。

重新标记阶段

在并发标记阶段由于垃圾回收线程与工作线程同时运行,工作线程会产生新的对象,也会使新的对象失去引用。所以在进行清理之前,CMS 还会对这些新产生变化的对象进行标记。

重新标记阶段,系统将会再次「Stop the world」,工作线程将会停止工作,垃圾回收线程开始标记那些新存活的对象和新垃圾对象。

重新标记

并发清理

并发清理阶段程序也能正常运行,垃圾回收线程则会清理被标记为垃圾的对象。

并发清理

为什么老年代的 Full GC 要比新生代的 Minor GC 慢很多倍,一般在 10 倍以上?

最后就能回答为什么老年代的垃圾回收速度会比新生代的垃圾回收速度慢很多倍?到底慢在哪里?这个问题了。

新生代的 Minor GC 执行速度很短,因为它只需要直接从 GC Roots 出发追踪哪些对象是存活的就行,新生代存活的对象是极少的,所以这个过程很快。完成对对象的标记之后,只需要将存活的对象一次性移动到 Survivor 区,一次性回收 Eden 和另一个 Survivor 区即可。

老年代的 Old GC 则分成初始标记、并发标记、重新标记和并发清理这四个阶段。其中并发标记阶段需要追踪老年代中中所有存活的对象,老年代中存活的对象比新生代多得多,用的时间也就更多。

在并发清理阶段也不是一次向回收一大片对象,一点点分散在各处的垃圾对象。清理完之后还需要整理一次内存碎片,将大量存活的对象移动到一起,此时还会「Stop the world」就更加慢了。

最后,并发清理阶段会有新的对象进入老年代,此时如果老年代的内存不足会引发了“Concurrent Mode Failure”问题,就会使用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程。