G1垃圾收集器入门教程(5)——逐步讲解G1垃圾收集器

逐步讲解 G1 垃圾收集器

G1 收集器采用不同的方法来分配堆内存。接下来,我们会逐步回顾 G1 系统,如以下图片所示。

1. G1 堆内存的结构

堆内存是一块内存区域(area),它又被分割成许多固定大小的区域(region)。

G1堆内存的结构

JVM 在启动时可以选择区域的大小。通常,JVM 会将堆内存分割成大约 2000 个大小在 1Mb 至 32Mb 之间的区域。

2. G1 堆内存的分配

实际上,这些区域会被映射成 Eden 区、Survivor 区和老年代内存空间的逻辑表示。

G1堆内存的分配

上图中的颜色表示哪个区域和哪个角色相关联。存活对象会从一个区域被排空(例如:复制或移动)至另一个区域。区域被设计成能够并行地收集垃圾,在此期间,可以停止所有其他应用程序的线程,也可以不停止所有其他应用程序的线程。

如上图所示,可以将区域分配成 Eden 区、Survivor 区和老年代的区域。此外,还有第四种类型的区域,被称为“巨型”区域。这些区域被设计用来存放体积等于标准区域容量 50% 或更大的对象。它们被存储为一系列相邻的区域。最后一种类型的区域是堆内存中尚未使用的区域。

注意:在撰写本文时,巨型区域的垃圾收集尚未达到最优化的性能。因此,你应该避免创建类似的巨型对象。

3. G1 中的年轻代

G1 会将堆内存分割成大约 2000 个区域。区域的最小容量为 1Mb,最大容量为 32Mb。蓝色区域存放老年代的对象,而绿色区域存放年轻代的对象。

G1中的年轻代

注意,这些区域不需要像较老的垃圾收集器那样是相邻的。

4. G1 中的年轻代垃圾收集

G1 会将存活对象排空(例如:复制或移动)至一个或多个 Survivor 区域。如果某些对象满足年龄阈值,那么 G1 就会将这些对象晋升至老年代区域。

G1中的年轻代垃圾收集

这样会造成“Stop The World(STW)”停顿时间。G1 会为下一次的年轻代垃圾收集计算 Eden 区和 Survivor 区的大小。G1 会保留记账信息,以便于帮助计算 Eden 区和 Survivor 区的大小。诸如停顿时间等性能目标也会被纳入考虑。

这种方法使得调整区域(region)的大小变得非常方便,可以根据实际需求扩大或缩小区域的大小。

5. 使用 G1 完成年轻代的垃圾收集

存活对象已经被排空至 Survivor 区域或者晋升至老年代区域。

使用G1完成年轻代的垃圾收集

在上图中,最近晋升的对象以深蓝色表示。Survivor 区域以绿色表示。

总之,使用 G1 进行年轻代的垃圾收集,可以概括为以下几点:

  • 堆内存是一块单独的内存空间,它被分割成若干个区域。
  • 年轻代的内存由一系列不相邻的区域构成。这样就使得年轻代能够非常方便地按照需要调整大小。
  • 年轻代垃圾收集,又被称作 Young GC,会产生 STW 事件。所有的应用程序线程在此期间都会停止运行。
  • 年轻代垃圾收集会通过多线程并行执行。
  • 存活对象会被拷贝至新的 Survivor 区域或者老年代区域。

使用 G1 进行老年代垃圾收集

类似于 CMS 收集器,G1 收集器也可以用于老年代垃圾收集,并且也被设计成一种低停顿时间的收集器。以下表格描述了 G1 在收集老年代的垃圾时将要经历的阶段。

G1 收集阶段 — 并发标记循环阶段

G1 收集器在收集堆内存的老年代的垃圾时,将要经历以下几个阶段。注意,某些阶段也是年轻代垃圾收集的一部分。

阶段 描述
(1)初始标记
(Stop the World事件)
这个阶段会发生 STW 事件。G1 收集器在这个阶段需要依托于一次普通的 Young GC。G1 会将可能引用老年代对象的区域标记成幸存区域(根节点区域)。
(2)扫描根节点区域 扫描引用至老年代的幸存区域。与此同时,应用程序也在不断地运行。这个阶段必须在 Young GC 发生之前执行完成。
(3)并发标记 在整个堆内存中查找存活对象。与此同时,应用程序也在不断地运行。这个阶段可以被年轻代的垃圾收集中断执行。
(4)重新标记
(Stop the World事件)
这个阶段会完成堆内存中存活对象的标记操作。G1 会使用一种称为初始快照(SATB)的算法,这种算法的速度比 CMS 收集器使用的算法要快得多。
(5)并发清理
(Stop the World事件)
① 对存活对象进行估算,然后完全释放区域的内存空间(会发生 STW 事件)。
② 清理记忆集合(RSet)(会发生 STW 事件)。
③ 重设空闲的区域,并且将它们返回至空闲列表(并发操作)。
(*)复制
(Stop the World事件)
这个阶段会将存活对象排空或拷贝至未使用的新区域之中,这些操作会造成 STW 事件。在对年轻代的区域进行这些操作时, 会记录[GC pause (young)]日志。在同时对年轻代和老年代的区域进行这些操作时,会记录[GC Pause (mixed)]日志。

逐步讲解 G1 的老年代垃圾收集

现在,我们来看看上面定义的几个阶段是如何在 G1 进行老年代垃圾收集时相互影响的。

6. 初始标记阶段

存活对象的初始标记依托于一次年轻代的垃圾收集。在日志中会记录GC pause (young)(inital-mark)信息。

初始标记阶段

7. 并发标记阶段

如果找到空闲区域(下图中用“X”表示的区域),那么在重新标记阶段就会将这些空闲区域立即移除。此外,还会计算“记账”信息,这些信息能够确定对象的存活度。

并发标记阶段

8. 重新标记阶段

G1 会移除空闲区域,然后回收它们的内存空间。现在,G1 会为所有区域计算区域存活度。

重新标记阶段

9. 复制/清理阶段

G1 会选择“活跃度”最低的区域,这些区域的垃圾收集速度是最快的。然后,这些区域的垃圾收集是和年轻代的垃圾收集同时进行的。这种行为在日志中表示为[GC pause (mixed)]。因此,年轻代和老年代都是同时进行垃圾收集的。

复制/清理阶段

10. 复制/清理阶段完成之后

G1 已经完成了被选中区域的垃圾收集,并且还整理了内存碎片,在下图中表示为深蓝色区域和深绿色区域。

复制/清理阶段完成之后

老年代垃圾收集总结

总的来说,我们可以将 G1 针对老年代的垃圾收集概括为以下几个关键点:

  • 并发标记阶段
    • 存活度信息是并发计算的,同时应用程序也在持续运行。
    • 这个存活度信息可以标识在排空造成的停顿时间之内最好回收哪些区域的内存空间。
    • 类似于 CMS 收集器,G1 的清除操作也没有停顿时间。
  • 重新标记阶段
    • 使用初始快照(SATB)算法,这种算法的速度比 CMS 使用的算法快得多。
    • 完全空闲的内存区域会被回收内存空间。
  • 复制/清除阶段
    • G1 会同时回收年轻代和老年代的内存空间。
    • G1 会根据存活度来选择老年代区域。