G1垃圾收集器入门教程(4)——回顾CMS垃圾收集器

并发标记清除(CMS)收集器(也被称为并发低暂停收集器)负责收集老年代的垃圾。CMS 在执行大部分的垃圾收集工作时会尝试和应用程序的线程并发工作,这样便能尽可能地减少由于垃圾收集而造成的停顿时间。通常,并发低暂停收集器不会复制或整理存活的对象。进行垃圾收集时,不会移动存活的对象。如果内存碎片的问题较为严重,那么可以分配一块更大的堆内存。

注意:年轻代的 CMS 收集器使用与并行收集器相同的算法。

CMS 收集阶段

CMS 收集器在堆内存的老年代上收集垃圾时,会执行以下阶段:

阶段 描述
(1)初始标记
(Stop the World事件)
在老年代中被“标记”为可达的对象包括那些可以从年轻代中获得的对象。相对于 Minor GC 的停顿时间来说,老年代垃圾收集的停顿时间通常很短。
(2)并发标记 为了并发查找可达对象,就需要遍历老年代的对象图,与此同时,Java 应用程序的线程也正在执行。从已标记的对象开始扫描,并且从 GC 根结点传递地标记所有可达对象。在 CMS 的并发阶段 2、3 和 5 期间,应用程序不会停止执行。在这些并发阶段期间,应用程序创建的任何对象(包括晋升的对象)都会被立即标记为存活。
(3)重新标记
(Stop the World事件)
在并发收集器已经完成跟踪某些对象之后,Java 应用程序的线程有可能会更新这些对象,因此 CMS 应该找出被并发标记阶段遗漏的这些对象。
(4)并发清除 收集在标记阶段被标识为不可达的对象。收集死亡对象会增加空闲的内存空间,CMS 会将这些空闲空间添加至一个空闲列表之中,稍后为新对象分配内存时可以使用这个空闲列表。此时,可能会合并死亡对象。注意,CMS 不会移动存活对象。
(5)复位 清除数据结构,准备下一次的并发收集。

回顾垃圾收集的步骤

接下来,我们要逐步回顾 CMS 收集器的工作过程:

1. CMS 收集器的堆内存结构

堆内存被分割成三块空间,如下图所示:

CMS的堆内存结构

年轻代被分割成一个 Eden 区和两个 Survivor 区。老年代是一块连续的内存空间。对象收集会就地完成。除非执行一次 Full GC,否则不会进行碎片整理。

2. CMS 如何收集年轻代的垃圾

在下图中,年轻代被标记为浅绿色,而老年代被标记为蓝色。当你的应用程序运行一段时间之后,堆内存的使用状况可能如下图所示。对象散布在整个老年代区域之中。

如何收集年轻代的垃圾

CMS 收集器会就地释放老年代对象的内存空间。这些对象不会被移动至其他地方。除非执行一次 Full GC,否则不会进行碎片整理。

3. 年轻代垃圾收集

收集器会将存活对象从 Eden 区和 Survivor 区拷贝至另一个 Survivor 区。任何较老的对象在达到它们的年龄阈值之后,都会被晋升至老年代。

年轻代垃圾收集

4. 年轻代垃圾收集之后

在对年轻代进行垃圾收集之后,Eden 区便会被清空,其中一个 Survivor 区也会被清空。

年轻代垃圾收集之后

在上图中,最近晋升的对象被标记为深蓝色。绿色的对象是幸存的年轻代对象,这些对象尚未被晋升至老年代。

5. 使用 CMS 收集老年代的垃圾

会发生两次“Stop the World”事件:初始标记和重新标记。当老年代达到一个特定的占用率时,就会触发 CMS 收集器。

使用CMS收集老年代的垃圾

  • 初始标记:在标记存活(可达)对象时,会造成短暂的停顿时间。
  • 并发标记:找出存活对象,与此同时,应用程序也会持续运行。
  • 重新标记:找出在并发标记阶段有可能遗漏的对象。

6. 老年代垃圾收集 — 并发清除

在前一个阶段没有被标记的对象会就地释放内存,但是不会进行内存碎片整理。

老年代垃圾收集 — 并发清除

注意:未标记对象 == 死亡对象

7. 老年代垃圾收集 — 清除之后

在并发清除阶段完成之后,你可以看到已经有大量内存被释放了。你还会注意到,没有进行任何的内存碎片整理。

老年代垃圾收集 — 清除之后

最后,CMS 收集器将会进入复位阶段,等待触发下一次垃圾收集的 GC 阈值。