八、垃圾收集高级

简介: 八、垃圾收集高级


👋垃圾收集高级

⚽️1. CMS

CMS 收集器是专为老年代空间设计的一个延迟极低的收集器,它通常会与一个稍微修改过的、用于 Young GC 的并行收集器(叫作 ParNew,而不是 Parallel GC)配对使用。

CMS 会在应用线程仍在运行的时候尽量多做一些工作,以便最大化地减少暂停时间。它使用的标记算法是三色标记,当然这就意味着在收集器正在扫描堆的同时,对象图可能会被修改。因此, CMS 必须对其记录进行修正,以避免破坏垃圾收集器的第二条规则,也就是把仍然活着的对象收集了。

这就导致了与并行收集器相比, CMS 所需的阶段更为复杂。这些阶段如下。

  1. 初始标记(initial mark)(STW)
  2. 并发标记(concurrent mark)
  3. 并发预清理(concurrent preclean)
  4. 重新标记(remark)(STW)
  5. 并发清除(concurrent sweep)
  6. 并发重置(concurrent reset)

垃圾收集在大多数阶段是与应用程序线程同时运行的,但是在初始标记和重新标记这两个阶段中,所有应用程序线程都必须停止。总的效果应该是用两个通常来说时间比较短的STW 暂停来代替一次长时间的 STW 暂停。

初始标记阶段的目的是为该区域内的垃圾收集提供一个稳定的起点集合,这些起点被称为内部指针,相当于用于收集周期的垃圾收集根。这种方法的优点是它使得标记阶段可以专注于单个垃圾收集池,而不必考虑其他内存区域。初始标记阶段结束后,并发标记阶段开始。本质上就是在堆上运行三色标记算法,记录以后可能需要修正的任何修改。

并发预清理阶段似乎试图尽可能地缩短会造成 STW 的重新标记阶段的长度。重新标记阶段使用卡表来修正可能会在并发标记阶段受 Mutator 线程影响的标记。

对大多数工作负载而言,使用 CMS 可以观测到如下影响:

• 应用程序线程不会停顿很久;

• 一次 Full GC 周期需要更多时间(以挂钟时间计算);

• 当 CMS 的垃圾收集周期运行的时候,应用程序的吞吐量会降低;

• 垃圾收集会使用更多的内存来记录对象信息;

• 整体来看,执行垃圾收集需要更多的 CPU 时间;

• CMS 不会对堆进行压缩,所以老年代的碎片会越来越多。

细心的读者会注意到,并不是所有这些影响都是正面的。请记住,垃圾收集并没有什么良方,只有针对工程师正在调优的具体工作负载所做出的一系列适当(或可以接受)的选择。

⚾️1.1 CMS是如何工作的

令人诧异的是, CMS 最常被忽视的一个方面是它的巨大优势,它在大多数时候是与应用程序线程并发运行的。 CMS 会默认使用一半的可用线程来执行垃圾收集的并发阶段,并将另一半留给应用线程来执行 Java 代码, 而这就不可避免地会涉及分配新对象。听上去很简单,但它有一个直接后果,即如果 Eden 区在 CMS 运行时被填满了,那会发生什么?

答案并不令人吃惊,因为应用程序线程无法继续,所以它们会暂停,并且在 CMS 运行时触发一次会导致 STW 的新生代收集(Young GC)。这次 Young GC 运行的时间通常会比并行收集器长,因为只有一半的 CPU 核心可用于 Young GC,另一半还在运行 CMS。

在 Young GC 结束时,通常有些对象会有资格晋升到 Tenured 区。因为这些对象需要在CMS 仍在运行的时候移动到 Tenured 区,所以两个收集器之间需要做一些协调工作。这就是 CMS 需要一个略有不同的新生代收集器的原因。

正常情况下, Young GC 只会把很少量的对象晋升到 Tenured 区, 而 CMS 老年代收集也能正常完成,释放 Tenured 区的空间。之后应用程序回归正常处理,所有的 CPU 核心也都可以释放出来供应用程序线程使用了。

但是,在分配率非常高时,可能在 Young GC 中导致过早晋升。这会引发一种情况,即 Young GC 有太多对象要晋升到 Tenured 区的可用空间中,如下图所示。

这种情况被称为并发模式失败(concurrent mode failure, CMF),除了回退到使用会造成STW 的 ParallelOld,此时 JVM 别无选择。实际上,分配压力如此之大,以致在所有的“净空间”被新晋升的对象填满之前, CMS 没有时间完成老年代的处理。

为了避免频繁出现并发模式失败, CMS 需要在 Tenured 区完全填满之前启动一次收集。至于在 Tenured 堆的占用达到何种水平时 CMS 将会启动,可以通过观察堆的行为来控制。它可能会受到开关的影响,初始值默认为Tenured 区的 75%。

堆的碎片化也会导致并发模式失败。与 ParallelOld 不同, CMS 在运行过程中不会对Tenured 区进行压缩。这意味着在 CMS 运行完成后, Tenured 区中的空闲空间并不是一个连续的块,晋升而来的对象必须被填充到现有对象之间的空隙中。

在某一时刻, Young GC 可能会遇到这样一种情况:由于 Tenured 区缺乏足够的连续空间来复制对象,因此对象无法晋升过去,如下图所示。

这就是由堆的碎片化引起的并发模式失败,和之前一样,唯一的解决办法是回退到使用ParallelOld(这是压缩的)进行一次 Full GC,从而释放出足够的连续空间以支持对象晋升。

无论是堆碎片化,还是 Young GC 的速度超过 CMS,需要回退到完全 STW 的 ParallelOld收集对应用程序而言都是重大事件。事实上,为避免遭受 CMF 而对使用 CMS 的低延迟应用程序进行调优本身就是一个重要的课题。

在内部, CMS 使用了一个内存块的空闲列表来管理可用内存。在最后的并发清除阶段,连续的空闲块将被 sweeper 线程合并。这是为了提供更大的空闲空间块,以避免由碎片化造成的 CMF。

然而, sweeper 会与 mutator 并发运行。因此,除非 sweeper 和分配器线程正确同步,否则刚分配的块可能会被错误地清除掉。为了防止出现这种情况, sweeper 在清扫过程中会锁住空闲列表。

⚾️1.2 用于CMS的基本JVM标志

CMS 收集器可用以下标志开启:

-XX:+UseConcMarkSweepGC

在现代版本的 HotSpot 上,这个标志也会激活 ParNewGC(与并行新生代收集器略有不同的变种)。

总的来说, CMS 提供了大量可以调整的标志(超过 60 个)。有时进行基准测试很有吸引力,因为它试图通过仔细调整 CMS 提供的不同选项来优化性能。千万要抵制这种诱惑,因为在大部分情况下,这实际上是“忽略大局”或“按照坊间传说调优”这些反模式的伪装。

⚽️2. G1

G1 是一款与并行收集器或 CMS 的风格都非常不同的收集器。它最初以高度实验性和不稳定的状态出现于 Java 6 中,但在 Java 7 的整个开发过程中经过了大量重写,直到 Java 8u40的发布才真正成为稳定的、可供生产使用的版本。

G1 最初是打算成为一款替代用的低延迟收集器,具有如下特性。

• 调优比 CMS 更容易;

• 不容易受到过早晋升的影响;

• 行为在大堆上能够更好地扩展(特别是暂停时间);

• 能够消除(或者能极大减少回退到)完全 STW 的收集。

然而,随着时间的推移, G1 逐渐被认为是一款通用的收集器,在更大的堆上有更少的暂停时间(越来越多的人认为这是“新常态”)。

G1 收集器在设计上重新思考了到目前为止我们都在用的分代的概念。与并行或 CMS 收集

器不同, G1 中的“代”没有专用的连续内存空间。此外,它也没有使用半空间堆布局。

⚾️2.1 G1堆布局和区域

G1 堆基于区域(region)的概念。这些区域的大小默认为 1 MB(在更大的堆上会更大)。区域的使用支持非连续的分代,从而使收集器可以不需要在每次运行时收集所有垃圾。

整体的 G1 堆在内存中仍然是连续的,只是组成每一代的内存不必再是连续的。

G1 堆基于区域的布局如下图所示。

G1 的算法支持区域的大小为 1、 2、 4、 8、 16、 32 或 64 MB 中的某个值。默认情况下,它期望堆中区域的数量在 2048 到 4095 之间,如果不在,它会调整区域的大小来实现这个目标。

要计算区域大小,可以计算 / 2048 这个值,并将结果取整数,选择离它最近的所允许的区域大小。区域的数量可以这样计算:

Number of regions = /

照例,可以通过运行时开关来修改这个值。

⚾️2.2 G1算法设计

从上层看这款收集器,可以得到以下信息:

• G1 使用了一个并发标记阶段;

• G1 是一款疏散收集器;

• G1 提供了“统计型压缩”(statistical compaction)。

在预热的同时,收集器会跟踪并统计每个垃圾收集执行周期有多少“典型”的区域可被收集。如果能收集足够的内存以平衡自上次垃圾收集以来分配的新对象,那 G1 就不会因为分配而失败。

TLAB 分配、疏散到 Survivor 空间以及晋升到老年代区,这些概念与我们已经接触过的其他 HotSpot 垃圾收集大体相似。

占用空间超过区域一半的对象被认为是巨型(humongous)对象。它们被直接分配在特殊的巨型区域中,该区域是空闲的连续区域,可以立即成为Tenured 区(而非 Eden 区)的一部分。

G1 仍然有由 Eden 和 Survivor 区域组成的新生代这个概念,当然,在 G1 中组成的代的区域是不连续的。新生代的大小是自适应的,会基于整体的暂停时间目标来调整。

回想一下,在介绍 ParallelOld 收集器时,我们曾说过“有极少数从老年代指向新生代对象的引用”。 HotSpot 使用一种叫作卡表的机制来帮助在并行和 CMS 收集器中利用这种现象。

G1 收集器有一个相关的功能来帮助区域进行跟踪。 记忆集(remembered set,通常只是称为 RSet)是每个区域都有的条目,它们会记录指向当前堆区域的外部引用。这意味着 G1不需要通过遍历整个堆来寻找指向某个区域的引用,而只需要检查RSets,然后扫描这些区域来寻找引用。

下图演示了 G1 是如何使用 RSets 来实现分配器和收集器之间的工作划分的。

RSets 和卡表这两种方法都可以帮助处理一个叫浮动垃圾(floating garbage)的垃圾收集问题。浮动垃圾产生的原因是有些对象本来应该是死的,但是在当前收集集合之外的死亡对象中仍然保留着对这些对象的引用。也就是说,如果是全局标记,那么可以看到这些对象是死的,但如果是一个局限性更大的局部标记,那可能会错误地报告这些对象是活的,这和当前使用的根集有关。

⚾️2.3 G1的各阶段

G1 可以划分为一系列阶段,和之前遇到的收集器(特别是 CMS)有点类似,具体如下。

  1. 初始标记(initial mark), STW
  2. 并发根扫描(concurrent root scan)
  3. 并发标记(concurrent mark)
  4. 重新标记(remark), STW
  5. 清理(cleanup), STW

并发根扫描是一个并发标记阶段,该阶段会扫描初始标记的 Survivor 区域以寻找指向老年代的引用。这个阶段必须在下一次 Young GC 开始之前完成。在重新标记阶段,标记周期完成。这个阶段还执行引用处理(包括弱引用和软引用),并进行与实现 SATB 方法有关的清理工作。

清理阶段大多是 STW 的,它包括处理记账信息和“擦洗” RSet。记账处理任务会识别现在已经完全空闲并准备好复用的区域(比如用作 Eden 区域)。

⚾️2.4 用于G1的基本JVM标志

在 Java 8 和更早的版本中,需要使用如下开关来启用 G1:

+XX:UseG1GC

回想一下, G1 是围绕暂停时间目标设计的。这使得开发人员可以指定应用程序在每个垃圾收集周期中应该暂停的最大时间。虽说是一个目标,但是 JVM 并不能保证应用程序能够达到这个目标。如果这个值设置得太低,那么垃圾收集子系统将无法达到目标。

控制 G1 收集器这一核心行为的开关是:

-XX:MaxGCPauseMillis=200

这意味着默认的暂停时间目标是 200 毫秒。因此,在实践中很难可靠地实现暂停时间小于100 毫秒,收集器也很难达到这样的目标。另一个可能有用的选择是修改区域的大小,可以这样覆盖默认算法:

-XX:G1HeapRegionSize=<n>

注意, 必须是 2 的幂,范围在 1 到 64 之间,表示一个单位为 MB 的值。

总的来说, G1 现在已经是一种稳定的算法了,并且得到了 Oracle 的完全支持(推荐从8u40 开始)。对于真正的低延迟工作负载,大部分情况下其表现不如 CMS,也不清楚单纯在暂停时间这方面, G1 是不是已经能够挑战像 CMS 这样的收集器。不过, G1 收集器仍然在不断改进,它也是 Oracle 的 JVM 团队在垃圾收集上的重点工程方向。

⚽️总结

本章重点介绍了 CMS 和 G1 收集器,G1是目前主流的收集器之一

👬 交友小贴士:

博主GithubGitee同名账号,Follow 一下就可以一起愉快的玩耍了,更多精彩文章请持续关注。

目录
相关文章
|
Java
JVM深入学习(十八)-如何查询当前jvm使用的垃圾回收器
如何查看使用的jdk用的垃圾回收器类型?
797 0
|
Java 算法 程序员
带你读《新一代垃圾回收器ZGC设计与实现》之一:垃圾回收器概述
JDK 11于2018年9月25日正式发布,这个版本引入了许多新的特性,其中最为引人注目的就是实现了一款新的垃圾回收器ZGC。
|
3月前
|
Arthas 监控 算法
JVM工作原理与实战(二十五):堆的垃圾回收-垃圾回收算法
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了垃圾回收算法评价标准、标记清除算法、复制算法、标记整理算法、分代垃圾回收算法等内容。
48 0
JVM工作原理与实战(二十五):堆的垃圾回收-垃圾回收算法
|
3月前
|
存储 监控 算法
JVM工作原理与实战(二十七):堆的垃圾回收-G1垃圾回收器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了G1垃圾回收器、G1垃圾回收器的回收方式、G1垃圾回收器执行流程、垃圾回收器的选择等内容。
57 0
|
3月前
|
机器学习/深度学习 监控 算法
JVM工作原理与实战(二十六):堆的垃圾回收-垃圾回收器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了垃圾回收器、Serial垃圾回收器、SerialOld垃圾回收器、ParNew垃圾回收器、CMS垃圾回收器、Parallel Scavenge垃圾回收器、Parallel Old垃圾回收器等内容。
34 0
|
3月前
|
存储 缓存 算法
五、垃圾收集基础
五、垃圾收集基础
37 3
|
存储 算法 Java
JVM学习日志(十三) G1垃圾回收流程 及 垃圾回收器总结
G1垃圾回收流程 及 垃圾回收器 总结 简述
182 0
JVM学习日志(十三) G1垃圾回收流程 及 垃圾回收器总结
|
算法 Java UED
JVM之垃圾回收器概述
JVM之垃圾回收器概述
|
8月前
|
存储 Java C#
C# 垃圾回收机制(GC) 的概述 资源清理 内存管理
C# 垃圾回收机制(GC) 的概述 资源清理 内存管理
|
监控 算法 Java
jvm之垃圾回收概述解读
jvm之垃圾回收概述解读