概览

收集器 收集区域 收集方式 多线程 STW 特征
Serial 新生代 标记-复制 × 占用内存小
ParNew 新生代 标记-复制 唯一搭配 CMS,后植入 CMS
Parallel Scavenge 新生代 标记-复制 精确控制吞吐量
Serial Old 老年代 标记-整理 × CMS 失败后备预案
Parallel Old 老年代 标记-整理 高吞吐量
CMS 老年代 标记-清除 首个并发回收的收集器

特点:

  • Serial 系列不支持多线程。
  • Parallel 系列追求高吞吐率。
  • 剩下 ParNew 与 CMS 搭配。

Serial

Serial 是采用标记-复制算法的单线程新生代收集器。在 JDK1.3.1 之前是 HotSpot 新生代收集器的唯一选择。与其搭配使用的有 Serial Old 和 CMS 收集器,但 Serial + CMS 的组合在 JDK8 中被声明废弃,在 JDK9 中被取消支持。

由于它是单线程收集器,它的工作过程必将带来 STW。但对于内存资源受限的环境,它是所有垃圾收集器中额外内存占用(Memory Footprint)最小的;对于单核处理器或核心数较少的环境,由于它没有任何线程切换的开销反而会获得最高的单线程收集效率。因此Serial 适用于运行环境线程数少、分配给 JVM 管理的内存不多的场景,如用户桌面程序、部分微服务应用等客户端模式下的应用程序。

ParNew

ParNew 是采用标记-复制算法的多线程新生代收集器。是 Serial 的多线程版本,它的工作方式和 Serial 基本完全一致,即使使用多线程进行 Minor GC,这个过程依旧需要 STW。它提供 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

ParNew 是 JDK7 之前首选的新生代收集器,仅因为它是目前唯一能和 CMS 配合的收集器。而随着 CMS 被 G1 继承和取代,JDK9 取消了 ParNew 和 Serial Old 的组合,此后 ParNew 变成 CMS 的一部分,因为二者只能互相搭配使用,因此 ParNew 成为第一款退出历史舞台的 HotSpot 虚拟机。

Parallel Scavenge

Parallel Scavenge 是采用标记-复制算法的多线程新生代收集器,其关注点在于尽可能提高用户程序的吞吐量。

对于垃圾收集程序而言,吞吐量 = 用户程序执行时间 / (用户程序执行时间 + 垃圾收集程序执行时间)

高吞吐量虽然不一定有良好的响应速度,但可以最高效率地利用 CPU 资源。因此 Parallel Scavenge 收集器适合后台运算而不需要太多交互的任务。它提供了两个参数用于精确控制吞吐量:

  • -XX:MaxGCPauseMillis:最大垃圾收集停顿时间,为大于 0 的毫秒数。

  • -XX:GCTimeRatio:程序运行时间与垃圾收集时间的比率,默认为 99,二者和为 100。

  • -XX:+UseAdaptiveSizePolicy:自调优开关,让 JVM 根据系统运行情况收集性能控制信息,自动设置垃圾回收参数。

与其配套的 Parallel Old 收集器在 JDK6 才开始提供。虽然在此之前搭配 PS MarkSweep 进行老年代收集,但由于 PS MarkSweep 与 Serial Old 实现几乎一致,因此都称为 Parallel Scavenge + Serial Old 组合。

Serial Old

Serial Old 是采用标记-整理算法的单线程老年代收集器,其主要作用是搭配 Serial 在客户端模式下使用。如果在服务端模式下工作可能有两种用途:

  1. 在 JDK5 及之前的版本中与 Parallel Scavenge 收集器搭配使用。

  2. 作为 CMS 发生失败的后备预案。

Parallel Old

Parallel Old 是采用标记-整理算法的多线程老年代收集器,直到 JDK6 开始提供,在处理器资源稀缺或吞吐量优先的场合,都可以考虑使用 Parallel Scavenge + Parallel Old 组合进行垃圾收集。

CMS

CMS(Concurrent Mark Sweep)是采用标记-清除算法的多线程老年代收集器,支持并发收集。其收集过程分为四个步骤:

  1. 初始标记:是根节点枚举操作,必须 STW。
  2. 并发标记:并发标记算法,使用增量更新的方式解决漏标问题,这样在并发标记时创建的对象也会被添加到重新标记的根节点中。
  3. 重新标记:为了保持一致性快照,必须 STW。
  4. 并发清除:由于 CMS 采用标记-清除算法,非移动式回收,不会对用户线程造成影响,所以可以并发执行。

由于 CMS 支持并发收集,导致 GC 的停顿时间非常短暂,因此 CMS 适合工作在关注服务响应速度的程序中,如 Web 服务器上。但 CMS 仍然存在三个较大的问题:

  • CPU 敏感:面向并发设计的垃圾收集器都是 CPU 敏感的,由于分出一部分线程进行垃圾收***导致用户程序变慢降低吞吐量。CMS 默认启用 (CPU cores + 3) / 4 个线程进行回收工作,CPU 核心数越低,造成的吞吐量下降就越大。

    为缓解该情况,JVM 提供了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS) 的 CMS 变种,通过让 GC 线程与用户线程交替运行的方式,增加停顿时间来换取吞吐量。但由于效果不明显在 JDK8 中被声明废弃,JDK9 中被废弃。

  • 浮动垃圾问题:由于并发清除阶段用户程序仍在运行,当垃圾回收的速度赶不上内存分配的速度,就出现“并发失败(Concurrent Mode Failure)”,此时就会触发一次 Full GC,并调用 Serial Old 整理内存。并发失败的情况有:

    1. 应用程序创建大对象,向老年代申请空间,此时空间不足。
    2. 新生代进行 Minor GC,向老年代申请分配担保确认,此时空间不足以进行分配担保。
    3. 方法区空间耗尽,通常 CMS 不会回收方法区,但是空间不足时会促发 Full GC 回收方法区。

    -XX:CMSInitiatingOccu-pancyFraction 参数设置老年代回收的阈值,JDK5 中默认为 68%,JDK6 中默认为 92%。
    CMSInitiatingPermOccupancyFraction 可以启用 CMS 对方法区进行收集。对方法区的收集和对老年代的收集是相互独立的。

  • 内存碎片问题。由于采用标记-清除算法,收集结束后会出现大量的内存碎片。当无法分配足够大的连续空间时,JVM 不得不触发一次 Full GC。例如如果 Minor GC 向老年代申请分配担保时发现内存足够,但是由于内存碎片的原因无法容纳对象,就会发生“晋升失败(Promotion Failed)”的错误。

    -XX:UseCMSCompactAtFullCollection 开关参数用于在 CMS 必须进行 Full GC 时开启内存整理功能。
    -XX:CMSFullGCsBeforeCompaction 参数用于要求 CMS 在执行若干次没有内存整理的 Full GC 之后,下一次进入 Full GC 之前进行碎片整理,默认为 0 表示每一次都整理。这两个参数都在 JDK9 中被废弃。

Garbage First

G1(Garbage First)是首个面向局部收集和基于 Region 内存布局的垃圾收集器。被官方称为“全功能的垃圾收集器”。是一款主要面向服务端应用的垃圾收集器。

基于 Region 的布局

G1 把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个区域根据需要扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。这使得收集器能够对扮演不同角色的区域采用不同的策略去处理。

同时,G1 专门设置了 Humongous 区域用于存储大对象。只要大小超过 Region 的一半就会被判定为大对象。每个 Region 大小根据 -XX:C1HeapRegionSize 设置。对于大小超过 Region 的对象,会使用连续 N 个 Humongous 区域进行存放。Humongous 区域在 G1 中一般被看作是老年代区域。

真是这种基于堆的布局使得 G1 可以面向堆内存的任何部分来组成回收集(Collection Set,CSet),衡量标准不再是那个分代,而是那块内存更值得回收,这就是 G1 收集器的 Mixed GC 模式。

分区下的记忆集

G1 的每个 Region 都会维护一个记忆集,记录别的 Region 指向自己的指针。这个记忆集实际上就是一个哈希表,键是其他 Region 的地址偏移,值是当前区域卡表页索引号的数组。Region 越多,跨区指针就会越多,同时由于 G1 存储所有的跨区指针,所以 G1 卡表的内存占用负担是非常中的。通常 G1 要消耗 Java 堆 10% ~ 20% 的额外内存来维持收集器工作。

可预测的停顿时间模型

G1 将 Region 视为单词回收的最小单元,同时 G1 会跟踪各个 Region 里面的垃圾收集的统计信息,然后建立一个以衰减平均值(Decaying Average)为基础的停顿时间预测模型。根据这个停顿模型,G1 可以预测每一块 Region 垃圾收集的收益(可回收的空间与回收时间的比值),然后按照收益的大小维护一个优先级列表。每次回收时根据用户设定允许的收集停顿时间,在停顿时间允许的范围内,挑选出回收价值最大的那些 Region 进行回收,这也是“Garbage First”的由来。

衰减平均值会受到新数据的影响,平均值代表整体平均状态,衰减平均值代表最近的平均状态,因此更能反映目前的回收效果。

并发标记

G1 通过原始快照(SATB)算法解决并发标记的漏标问题。对于新增加对象的处理,G1 在每个 Region 上设计了两个名为 TAMS(Top at Mark Start) 的指针,将 Region 的一部分空间划分出来用于新对象的分配。G1 收集时默认这个地址上的对象都是被隐式标记过的,不属于垃圾对象。

运行过程

G1 的运行过程主要分为四个步骤:

  1. 初始标记:进行根节点枚举和设置 TAMS 指针,基本没有额外停顿。
  2. 并发标记:进行并发可达性分析,耗时较长,但是与程序并发执行,使用 SATB 处理漏标问题。
  3. 最终标记:保持一直性快照,必须 STW。
  4. 筛选回收:对各个 Region 的回收价值进行统计排序,根据用户期望停顿时间制定回收计划。回收 Region 时会把存活对象复制到空的 Region 中,再清理掉原来整个 Region 的空间。由于复制操作涉及到对象的移动,必须暂停用户线程。

筛选回收之所以不设计为并发执行,是因为 G1 的整体理念是在用户允许的停顿时间内进行垃圾回收,回收时间是可控的,因此,不必过分苛求低延迟垃圾回收。同时,牺牲小部分 STW 时间可以提高垃圾回收的吞吐量,这使得 G1 成为“全能垃圾收集器”。

G1 与 CMS

二者的共同点:

  • 都是支持并发收集的垃圾收集器。
  • 都非常成功,使用非常普遍,一个是昔日的王者,一个是新晋的王者。

G1 的优势:

  • 可以指定最大停顿时间。
  • 内存按区域划分,按收益动态回收垃圾更加高效。
  • 使用标记-复制算法,不会存在内存碎片问题。
  • 使用原始快照并发标记速度更快。
  • 不会那么容易触发 Full GC。

CMS 的优势:

  • 更低的内存消耗,记忆集不需要存储新生代对老年代的引用,更加符合对象朝生夕灭的规律。
  • 更低的执行复杂,少了非常多额外的运算和处理。
  • 回收阶段可以并发执行,垃圾收集的延迟非常低。

G1 作为 CMS 的替代品,依旧会不断更新与完善,因此,G1 的短板会不断缩小,二者的差距会不断增大。


作者:辐射工兵
链接:https://juejin.cn/post/6993308706410594311