1

1 G1 垃圾回收器

garbage-first (G1)收集器是一个服务器风格的垃圾收集器,针对具有大内存的多处理器机器。它在同时实现高吞吐量的情况下,以很高的概率满足垃圾收集(GC)暂停(STW:stop the word)时间目标。G1垃圾收集器在Oracle JDK 7 update 4和更高版本中得到了完全支持。G1收集器专为以下应用程序设计:

  • 可以向 CMS 垃圾回收器一样和用户线程并行。
  • 不会有长时间的 GC 停顿,且空闲空间更紧凑。
  • 需要更多可预测的GC暂停时间。
  • 不希望牺牲大量吞吐量
  • 不需要一个很大的 java 堆

G1 垃圾回收器是打算作为 CMS 垃圾回收器的替代品的。对比 G1 和 CMS,有些差异使得 G1 是一个更好的选择。

第一个区别是,G1 是一个压缩回收器。G1 进行了充分的压缩,以避免使用细粒度的自由列表来分配空间,而是使用区域(region)。这在很大程度上简化了回收器的部分,并且基本上消除了 CMS 的碎片问题。

第二个区别是,G1 提供了比 CMS 回收器更可靠预测停顿时间,这让用户可以指定需要的停顿时间。

1.1 G1操作概述

老的垃圾回收器(如:serial、paralle、CMS)都是把内存分为三个区域:年轻代young(年轻代又分为eden、S1、S2三个区域),老年代 old gerneration,永久代 permanent。

G1 采用一种不同的方法。

G1 中的堆被分为一系列相同大小的堆区域(region,也有人叫块),每个区域都是连续的虚拟内存。这一系列的堆区域,和老的垃圾回收器一样,被设定为三种角色(eden、survivor、old),但是他们的大小不固定,这给内存使用提供了更大的弹性。

当执行垃圾收集的时候,G1 的操作方式和 CMS 类似。G1 执行一个全局的并发标记阶段,来确定堆中存活的对象。标记阶段结束之后,G1 知道哪个区域大部分是空的(我理解是垃圾比较多的区域)。G1 会优先回收这些区域,这样就能够释放更多的空闲空间。这就是为什么这个垃圾回收器叫 G1 Garbage-First。

顾名思义,G1 将收集和压缩活动集中在堆中可能充满可回收对象(垃圾)的区域。G1 使用一个暂停预测模型来满足用户预设的停顿时间目标,并根据指定的停顿时间来选择要收集的一系列区域。

G1 使用清除算法来回收被标记为可回收的区域。G1 将 1 个或者多个区域的存活对象拷贝到另一个单独的区域。在这个过程中,即压缩了内存又释放了内存。这个清除操作在多处理器上并行执行,以减少停顿时间并提高吞吐量。因此,对于每个区域的垃圾回收,G1 持续不断减少碎片,且在用户定义的停顿时间内工作。这超出了前面两种垃圾回收器的能力。CMS 回收器回收的时候不压缩。ParallelOld 回收器回收的时候压缩整个堆,这不可避免造成很大的停顿时间。

G1 不是一个实时垃圾回收器,知道这点很重要。这意味着可以很大概率满足用户设定的停顿时间,但不是 100% 保证能达到。基于过去的回收数据,G1 评估回收多少 region,可以达到用户设定的停顿时间。因此,回收器对于手机区域的成本有一个相当精确的模型,他使用这个模型来决定在确定停顿时间目标下,收集哪些区域以及收集多少个区域。

注意: G1 有并发(与应用程序线程一起运行,例如,细化、标记、清理)和并行(多线程,例如,停止世界)两个阶段。完整的垃圾收集仍然是单线程的,但是如果适当调优,您的应用程序应该避免完整的gc。

1.2 G1 足迹

如果您从 ParallelOldGC CMS 集器迁移到G1,您可能会看到更大的 JVM 进程。这在很大程度上与“会计”数据结构有关,如 标记集合(rSet) 和 回收集合(cSet)。

1.3 推荐使用 G1 的场景

G1 的第一个重点就是针对这样的场景:要求运行需要大堆且 GC 延迟有限的应用。这意味着堆大小在 6GB 左右或者更大,稳定和可预测停顿时间在 0.5 秒以下。

不管是使用 CMS 还是 ParallelOld 回收器的应用,如果有以下特点,切换成 G1,会有一定的收益:

  • Full GC 耗时太长或者太频繁。
  • 对象分配率或提升率差异较大。
  • 回收或者压缩停顿时间太长(超过 0.5 或者 1 秒)

注意:如果您正在使用 CMS 或 ParallelOldGC,并且您的应用程序没有经历长时间的垃圾收集暂停,那么使用当前的收集器是可以的。更改到 G1 收集器并不是使用最新 JDK 的必要条件。

2 回顾 CMS 垃圾回收器

2.1 回顾分代 GC 和 CMS

The Concurrent Mark Sweep (CMS) 回收器(也称为并发低暂停回收器)收集老年代(tenured)。它通过让大部分垃圾回收工作和用户线程并行,试图最小化由垃圾回收引起的停顿时间。通常,CMS 不会复制和压缩存活对象。这是一个不会移动存活对象的垃圾回收器。如果碎片成为一个问题,分配一个更大的堆。

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

2.2 CMS 回收阶段

CMS收集器在堆的老一代上执行以下阶段:

阶段描述
初始标记(STW)老代中的对象,包括年轻代中可能可访问的对象被“标记”为可访问(可达性分析的可达)。相对于较小的收集暂停时间,这个阶段暂停时间的持续时间通常较短。
并发标记在用户线程在执行的同事,遍历老年代中的对象图以查找可达对象。从已标记对象开始扫描,且遍历标记从 root 对象开始可达的对象。在阶段2、3、5中新增的对象,还有这些阶段过程中被分配的对象马上被标记为存活对象
重新标记(STW)查找并发标记阶段由于 Java 应用程序线程在并发收集器完成跟踪对象之后对对象进行更新而遗漏的对象。
并发清除收集在标记阶段标识为不可达的对象。死对象(不可达对象)的集合将该对象的空间添加到空闲列表中,以供以后分配。死亡对象的合并可能发生在这一点上。注意,可达对象不会移动。
重置通过清除数据结构为下一次并发收集做准备。

2.3 回顾垃圾回收步骤

  1. CMS 回收器的堆结构

年轻代被分为 eden 区和两个 survivor 区域。老年代是一个连续的空间。对象在原地被回收。除非有 fullGC,否则不会有压缩。

  1. CMS 中年轻代 GC 是怎么工作的

年轻代被标记为绿色,老年代标记为蓝色。下图可能就是应用程序已经运行一段时间之后 CMS 内的样子。对象分散在老代区域周围。

使用 CMS,老年代对象被原地释放。它们不会被移动。除非有一个完整的GC,否则空间不会被压缩。

  1. 年轻代收集

活动对象从 eden 区和 S(survior)区域 复制到另一个 S 空间。任何已经达到老化阈值(默认配置是 15)的旧对象都被提升到老年代。

  1. 年轻代收集完之后

eden 区域被清空,以及其中一个 S 区域被清空。

新提升到老年代的对象在图中显示为深蓝色。绿色的对象是还没有提升到老年代的年轻代对象。

  1. 用 CMS 收集老年代

发生 STW 的两个时间点:初始标记和重新标记。当老年代空间占用率达到一定的比率(可配置)时,CMS 垃圾收集启动。

(1)初始标记是一个短暂的暂停阶段,这阶段标记可触达的对象;(2)并发标记找到应用继续运行期间可触达的对象;(3)最后,重新标记阶段,查找在第(2)阶段遗漏的对象。

  1. 老年代收集——并发清理

未在前面几个阶段被标记的对象视为垃圾对象,会被释放。没有压缩。

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

  1. 老年代收集——清理之后

在清理阶段完成之后,你可以看到很多的内存区域都被释放出来了。你能注意到一直没有进行压缩。

最后,CMS收集器将通过(5)重设阶段,等待下一次达到 GC 阈值。

3 G1 垃圾回收的步骤

G1 收集器使用一种不同的方法回收堆对象。下面的图片将一步一步给你展示 G1 回收步骤。

  1. G1 堆结构

堆是一块固定的内存区域,它被划分为固定大小的小区域。

小区域大小通过 JVM 启动的时候设定。JVM 通常以 2000 个区域为目标,大小从 1 到 32Mb 不等。

  1. G1 堆分配

事实上,这些区域被逻辑划分为 eden、survivor 和老年代区域。

图片中的颜色显示了哪个区域与哪个角色相关联。活动对象被从一个区域疏散(即复制或移动)到另一个区域。在停止(STW)或者不停止用户线程的情况下,区域被设计成与所有其他应用程序线程并行收集。

如图所示,可以将区域分配为Eden、survivor和老年代区域。此外,还有第四种类型的区域被称为大对象区域。这些区域被设计用来容纳标准区域大小的 50% 或更大的对象。它们被存储为一组相邻的区域。最后一种类型的区域可能是堆中未使用的区域。

注意:在撰写本文时,收集大对象还没有优化。因此,应该避免创建这种大小的对象。

  1. 用 G1 收集年轻代

整个堆被分为将近 2000 个区域。最小的 1M,最大的 32 M。蓝色区域持有老年代对象,绿色区域持有年轻代对象。

注意:相比于以前的垃圾回收器来说,区域不强制要求空间连续。

  1. G1 的年轻代垃圾回收

存活对象被疏散(复制或者移动)到一个或者多个 survivor 区域。如果超过年龄阈值,一些对象会被提升到老年代区域。

这个方法使得重新设置区域大小更容易,让他们更大或者更小也是必须的。

这个阶段会发生 STW。eden 和 survivor 区域为了下次年轻代 GC,大小会被计算。计算信息会被保存,以帮助计算大小。目标停顿时间等会被考虑进去。

  1. 结束年轻代垃圾回收

存活对象已经被疏散到 survivor 区域或者老年代区域。

现在提升到老年代对象标记为深蓝色。survivor 区域被标记为绿色。

总结,年轻代垃圾回收:

  • 堆是一整块区域,但被分为更小的区域。
  • 年轻代内存是一系列不连续的区域组成的。这让调整大小变得更容易。
  • 年轻代垃圾回收,会发生 STW。年轻代垃圾回收会停止所有用户线程。
  • 年轻代垃圾回收你用多个线程并发执行的。
  • 存活对象会被复制到 survivor 区域或者老年代区域。

3.1 G1 的老年代回收

和 CMS 回收器一样,对于收集老年代,G1 回收器目标也是成为一个低停顿时间的垃圾回收器。下面的表格描述了 G1 的各个阶段。

G1 收集老年代的时候,有以下阶段。注意:有些阶段是年轻代垃圾回收的一部分。

阶段描述
(1)初始标记(STW)这个阶段会发生STW,也是年轻代垃圾回收的一步。标记存活区域(根区域),这些区域可能有对老年代对象的引用。
(2)根区域扫描扫描 survivor 区域,查找到老年代的引用。这和用户线程同时进行。这个阶段必须在年轻代垃圾回收发生之前。
(3)并发标记查找整个堆中的存活对象。这和用户线程同时进行。这个阶段可以被年轻代垃圾回收打断。
(4)重新标记(STW)完成堆中存活对象的标记。使用一个叫SATB(snapshot-at-the-beginnint)的算法,这个算法比 CMS 中用的算法快。
(5)清除(STW且和用户线程并发)1)对存活对象和空区域进行计算。(STW)。2)清除记录集(STW)【啥意思?】。3)重设空区域并将其返回空闲列表(并发)
(*)复制(STW)这个阶段,计算停顿时间并复制存活对象到新的未使用区域。这可以使用年轻代完成,log标记为[GC Pause (young)]。或者使用年轻代和老年代,log标记为[GC Pause(mixed)]

以下为老年代回收的步骤:

定义了这些阶段之后,让我们看看它们如何与 G1 收集器中的老一代交互。

  1. 初始标记阶段

承载到年轻代垃圾回收之上,初始标记阶段标记存活对象。log标记为 [GC pause (young) (initial-mark)]

  1. 并发标记阶段

如果发现空区域,标记为 X,他们在重新标记阶段被立即删除。并且,这个阶段还计算了表示存活力的“会计”信息。

  1. 重新标记阶段

空区域被删除和回收。所有的区域活力(我理解为存活对象比例)被计算。

PS:对比阶段 7 来看这张图。

  1. 复制/清理阶段

G1 会选择活力最低的区域,这些区域能被更快的回收。然后在年轻代回收的时候同时回收这些区域。log 标记为[GC pause (mixed)]。因此年轻代和老年代是同时回收的。

  1. 复制/清理阶段之后

被选中被清理的区域,被清理和压缩之后会被标记为深蓝色区域和深绿色区域。如下图。

3.2 总结老年代GC

一些关于 G1 老年代垃圾回收的关键点:

  • 并发标记阶段

1)活力计算和用户线程并行

2)在疏散暂停时间的同时,计算哪些区域的活力信息区分哪些区域要被回收

3)没有像 CMS 的清理阶段

  • 重新标记阶段

1)使用 SATB 算法,这种算法比 CMS 用的算法快得多。

2)完全空的区域被回收,虽然这个阶段叫重新标记

  • 复制/清理阶段

1)年轻代和老年代被同时回收

2)老年代区域的选择是基于活力值的

4 最佳实践(忽略命令行操作部分)

一些你使用 G1,需要遵循的实践。

4.1 不要设置年轻代区域大小

通过 -Xmn 显式设置年轻代的大小会干扰G1收集器的默认行为。

  • G1 会不再尊重设定的目标停顿时间。所以本质上,设置年轻代大小与目标停顿时间冲突。
  • G1 不再能够根据需要扩展和收缩年轻代空间。由于大小是固定的,所以不能对大小进行更改。

4.2 响应时间指标

与其使用平均响应时间(ART)作为设置 XX:MaxGCPauseMillis=<N> 的度量标准,不如考虑设置在 90% 或更多的时间内满足目标的值。这样,90% 的用户请求不会经历比目标停顿时间更长的停顿。记住一件事,目标停顿时间是个目标,这个目标不保证 100% 满足。

4.3 什么是疏散失败?

这是当 JVM 在对 survivor 对象和提升对象进行 GC 期间耗尽堆区域时发生的提升失败。堆已经满了,不能再扩充了。如果使用了参数:XX:+PrintGCDetail,这个时候 log 会打印 to-space overflow。这个操作是昂贵的。

  • GC仍然必须继续,因此必须释放空间。
  • 未成功复制的对象必须被永久保存在适当的位置。
  • 对 CSet 中区域的 rset 的任何更新都必须重新生成。
  • 所有这些步骤都是昂贵的。

4.4 怎么避免疏散失败

考虑以下操作避免疏散失败

  • 提升堆大小

1) 增大 -XX:G1ReservePercent=n 这个值的大小,默认是 10.

2)G1 通过尝试保留空闲的预留内存来创建一个假天花板,以防需要更多的“to-space”。

  • 尽早开始标记周期。
  • 加大并发线程数,参数:-XX:ConcGCThreads=n

4.5 完整 G1 参数列表

选项和默认值描述
-XX:+UseG1GC使用 G1 垃圾回收器
-XX:MaxGCPauseMillis=n目标停顿时间 这是个软目标,JVM 会尽可能满足它
-XX:InitiatingHeapOccupancyPercent=n堆使用比率,超过这个比率会触发 G1 GC。GC 使用整个堆的使用比率,而不是某个区域的比率。值为 0 表示进行恒定 GC 循环。默认是 45
-XX:NewRatio=n年轻代/老年代空间比率。默认是 2
-XX:SurvivorRatio=neden/survivor区域空间比例。默认是 8
-XX:MaxTenuringThreshold=n保留期年龄阈值(超过阈值,提升到老年代)。默认 15
-XX:ParallelGCThreads=n垃圾回收过程中并发线程数。默认值随着使用 JVM 平台的不同而变化。
-XX:ConcGCThreads=n并发垃圾回收使用的线程数。默认值随着使用 JVM 平台的不同而变化
-XX:G1ReservePercent=n减少提升失败比率而设置的假天花板的堆的大小。默认值是 10
-XX:G1HeapRegionSize=n在G1中,Java堆被细分为大小一致的区域 这将设置各个子区域的大小 该参数的默认值是根据堆大小确定的 最小值为 1Mb,最大值为 32Mb

原文:https://www.oracle.com/techne...

https://docs.oracle.com/en/ja...


程序员伍六七
201 声望597 粉丝