深入浅出 JVM 之垃圾回收-垃圾回收器

深入浅出 JVM 之垃圾回收-垃圾回收器

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

系统环境:

  • JDK 1.8

参考地址:

深入浅出 JVM 系列文章

一、垃圾回收器 GC 是什么

GC (Garbage Collection,垃圾回收) 是一种自动内存管理机制,旨在自动回收不再使用的对象所占用的内存空间,从而避免内存泄漏和溢出等问题。通过周期性地扫描应用程序的堆空间,垃圾回收器能够识别出那些不再被引用的对象,并将其占用的内存空间释放回收。

在 Java 应用程序中,堆空间是所有 Java 对象的存放地,垃圾回收器的核心任务就是管理和回收这部分内存。JVM 提供了多种垃圾回收器实现,包括但不限于串行垃圾回收器、并行垃圾回收器以及并发垃圾回收器。这些不同的回收器采用了各自的回收策略和算法,能够针对不同应用场景和内存需求提供优化支持。

垃圾回收器的自动化特性极大地减轻了开发人员的负担,使其可以更加专注于业务逻辑的实现,而无需过分担心内存管理的问题。这种自动化的内存管理不仅有助于提高应用程序的安全性和稳定性,还能降低程序崩溃的风险。然而,值得注意的是,频繁或不当的垃圾回收活动也可能对应用程序的性能造成不利影响。因此,在开发 Java 应用程序时,选择合适的垃圾回收器类型,以及进行合理的配置就显得尤为重要,这有助于在保证内存管理效率的同时,维持或提升应用的性能和用户体验。

二、垃圾回收器 GC 分类

2.1 垃圾回收方式类型

垃圾回收器依据其工作方式主要可以分为三种模式: 串行 (Serial)并行 (Parallel)并发 (Concurrent),并且每种模式都有其特定的工作原理和适用场景。

■ 串行 (Serial)

在串行模式下,垃圾回收线程和用户线程会交替执行,即当垃圾回收开始时,所有的用户线程会被暂停 (Stop The World),直到垃圾回收完成才会恢复 (尽管垃圾回收线程本身是单线程的,但这并不意味着整个环境必须是单核 CPU 的,即使在多核 CPU 环境中,垃圾回收也只会使用一个核心来执行)。

■ 并行 (Parallel)

在并行模式下,垃圾回收会利用多核 CPU 的优势,通过多个垃圾回收线程并行工作来加速回收过程。在此期间,所有用户线程同样会被暂停,直至垃圾回收过程结束。这种方式能够在较短的时间内完成大量数据的回收,所以特别适合于大规模数据处理的应用场景。

■ 并发 (Concurrent)

在并发模式下,允许垃圾回收线程与用户线程同时运行,即在垃圾回收的过程中,应用程序仍然可以继续执行,也就是同一个时刻 CPU0 上执行用户线程,CPU1 上有可能执行垃圾回收线程。这种方式通过减少因垃圾回收导致的应用程序暂停 (Stop The World),提高了系统的响应速度和用户体验。并发垃圾回收器通常适用于那些对响应时间要求较高的应用。

2.2 常用垃圾回收器分类

常用的 GC 按照 串行并行并发 进行分类,可以分为:

  • 串行回收器: SerialSerial Old
  • 并行回收器: ParNewParallel ScavengeParallel Old
  • 并发回收器: CMSG1

2.3 常用垃圾回收器与分代之间的关系

不同的 GC 都有各自的作用范围,比如:

  • 新生代回收器 (Minor GC/Young GC): 只会发生在新生代。
  • 老年代回收器 (Major GC/Old GC): 只会发生在老年代,目前只有 CMS 垃圾回收器会有单独回收老年代垃圾的行为。
  • 混合回收器 (Mixed GC): 发生在整个新生代以及部分老年代。比如 G1 垃圾回收器就是一个混合回收器。
  • 整堆回收器 (Full GC): 发生在整个堆和方法区中,一般老年代满了内存不足时就会触发。

常用的 GC 按照分代进行划分,如下:

  • 新生代回收器: SerialParNewParallel Scavenge
  • 老年代回收器: CMS
  • 混合回收器: G1
  • 整堆回收器: Serial OldParallel Old

2.4 常用垃圾回收器组合关系

GC 通常根据堆内存的不同分代来设计,每种回收器可能专注于新生代、老年代或整个堆内存的管理。为了适应不同的应用需求,多种垃圾回收器可以组合使用,形成高效的内存管理策略。以下是几种常见的垃圾回收器组合方式:

  • Serial + Serial Old
  • Serial + CMS
  • ParNew + Serial Old
  • ParNew + CMS
  • Parallel Scavenge + Serial Old
  • Parallel Scavenge + Parallel Old
  • G1

注:

  • CMS 属于老年代垃圾回收器,只能回收老年代,当 CMS GC 执行清理后仍然空间不足,将会执行 Full GC,这时会使用 Serial GC 进行清理;
  • 由于维护和兼容性测试的成本,在 JDK 8 时将 Serial/CMS 和 ParNew/Serial Old 这两个组合声明为废弃 (JEP173),并在 JDK9 中完全取消了这些组合的支持 (JEP214);
  • 在 JDK14 中弃用了 Parallel Scavenge/Serialold GC 组合 (JEP366),并且删除 CMS 垃圾回收器 (JEP363);

三、JDK 版本与 GC

3.1 不同版本的默认 GC

JDK 版本 默认使用的 GC (Server模式)
JDK 6/7 Parallel Scavenge + Serial Old
JDK 8 Parallel Scavenge + Parallel Old
JDK 9/11/17 G1

3.2 如何查看当前 JDK 支持的有哪些 GC

查看当前 JDK 支持的 GC 的命令:

1$ java -XX:+PrintFlagsFinal -version | grep 'bool Use' | grep 'GC ' | grep -v java

显示的结果如下:

 1openjdk version "11.0.14.1" 2022-02-08 LTS
 2OpenJDK Runtime Environment 18.9 (build 11.0.14.1+1-LTS)
 3OpenJDK 64-Bit Server VM 18.9 (build 11.0.14.1+1-LTS, mixed mode, sharing)
 4     bool UseAdaptiveSizePolicyWithSystemGC        = false                                     {product} {default}
 5     bool UseConcMarkSweepGC                       = false                                     {product} {default}
 6     bool UseG1GC                                  = true                                      {product} {ergonomic}
 7     bool UseMaximumCompactionOnSystemGC           = true                                      {product} {default}
 8     bool UseParallelGC                            = false                                     {product} {default}
 9     bool UseParallelOldGC                         = false                                     {product} {default}
10     bool UseSerialGC                              = false                                     {product} {default}
11     bool UseShenandoahGC                          = false                                     {product} {default}

3.3 如何查看当前 JDK 使用的 GC

使用查看 Java 进程 ID 的命令:

1$ jps

显示的结果如下:

130598 test-1.0.jar

然后执行输出当前的 JDK 版本和使用的 GC 的命令:

1$ jhsdb jmap --heap --pid 30598

显示的结果如下:

1Attaching to process ID 30598, please wait...
2Debugger attached successfully.
3Server compiler detected.
4JVM version is 11.0.14.1+1-LTS
5
6using thread-local object allocation.
7Garbage-First (G1) GC with 2 thread(s)
8
9......

四、评估 GC 的性能指标

4.1 评估的指标

评估 GC 性能的关键在于多个维度的考量,确保既能够高效地管理内存,又不会对应用程序的正常运行造成显著影响。主要评估指标包括:

  • 垃圾回收时间 (GC Time): 表示垃圾回收操作消耗的时间总和。较短的垃圾回收时间意味着应用程序有更多时间执行业务逻辑,从而提高整体性能。
  • 垃圾回收频率 (GC Frequency): 指垃圾回收器进行垃圾回收的频率。通常,垃圾回收频率越低,暂时时间越短,应用程序的性能就越好。
  • 吞吐量 (Throughput): 指单位时间内垃圾回收器能够处理的垃圾量。通常,吞吐量越高,垃圾回收效率就越高,应用程序的性能就越好。
  • 暂停时间 (Pause Time): 指垃圾回收器进行垃圾回收时暂停应用程序执行的时间。通常,暂停时间越短,应用程序的响应时间就越短,性能就越好。
  • 内存占用 (Memory Footprint): 指应用程序在运行过程中占用的内存大小。较小的内存占用不仅有利于提高系统的整体性能,还能减少硬件成本和能源消耗。

通过综合分析这些指标,开发人员和运维团队可以更准确地评估垃圾回收器的表现,进而采取适当的调优措施。比如,调整堆大小、选择不同的垃圾回收算法或者修改垃圾回收参数,从而实现对应用程序的性能和稳定性进行优化。

4.2 吞吐量

在 GC 相关概念中的吞吐量指的是,在一定时间范围内,垃圾回收器能够处理的有效工作量 (有效工作量指的是应用程序实际运行所占的时间)。GC 吞吐量通常用百分比来表示,即垃圾回收器处理有效工作量的时间占应用程序总运行时间的比例。

吞吐量计算公式为: 吞吐量 = 用户线程运行时间 / (用户线程运行时间 + GC 线程运行时间)。比如,程序运行了 99s,GC 执行耗时 1s,则吞吐量为 99/(99+1)=99%

吞吐量是评估垃圾回收器性能的一个重要指标。如果垃圾回收器的吞吐量高,说明它能够高效地回收垃圾,同时对应用程序的影响较小,因此可以提高应用程序的性能和响应速度。

通常来说,吞吐量高的垃圾回收器在进行垃圾回收时会造成较长的停顿时间,因为垃圾回收器需要将所有的应用程序线程都暂停下来才能进行垃圾回收。因此,在选择垃圾回收器时需要根据应用程序的性质进行评估和选择。

4.3 暂停时间

GC 暂停时间是指垃圾回收器在进行垃圾回收时,停止应用程序运行的时间。当垃圾回收器在进行垃圾回收时,需要扫描整个堆空间,标记存活对象,回收无用对象等操作,这个过程中必须将所有应用程序线程暂停,以避免应用程序在垃圾回收期间访问已被回收的内存,导致程序崩溃或者发生未定义行为。

暂停时间是评估垃圾回收器性能的重要指标之一。暂停时间越短,就越能满足对实时性能和用户体验的要求。一些应用程序 (如金融交易、游戏等) 需要高度的实时性能和响应速度,对暂停时间的要求非常高。而一些后台处理型应用 (如批量数据处理、日志分析等) 则可以容忍较长的暂停时间。

暂停时间是与垃圾回收算法和垃圾回收器的实现有关的,因此不同垃圾回收算法和垃圾回收器的暂停时间也会有所不同。

4.4 吞吐量 VS 暂停时间

吞吐量 (Throughput) 和暂停时间 (Pause Time) 是垃圾回收器性能评估中的两个重要指标,它们之间存在着一定的权衡关系,这意味着优化其中一个指标通常会影响另一个指标。例如:

  • ① 为了提高吞吐量,那么必然需要降低内存回收的执行频率,但是这样会导致 GC 需要更长的暂停时间来执行内存回收。
  • ② 为了提高吞吐量,可以增加并行处理垃圾的线程数量,这将增加垃圾回收器的并行处理能力,从而提高吞吐量。但是,这也可能导致增加垃圾回收器的暂停时间,因为多个线程需要协调和同步。
  • ③ 为了降低暂停时间,常用的做法是将一次很长的暂停时间分为多次,增加 GC 执行的频率,从而降低每次 GC 时的暂停时间,不过这样做可能会导致程序吞吐量下降。

因此,在优化垃圾回收器性能时,需要根据应用程序的需求和性能要求,综合考虑吞吐量和暂停时间之间的权衡关系,选择合适的垃圾回收器以及合理的 GC 配置。

五、常用的 GC

5.1 Serial 回收器

Serial 回收器简介

Serial GC 是在 HotSpot 虚拟机中历史最为悠久的回收器,同时也是一款单线程的串行回收器,在进行回收时会触发 Stop The World,这时会造成全部的用户线程暂停,直至回收结束。

而且 Serial GC 是一个统称,其可以划分为新生代回收器和老年代回收器,我们常称新生代回收器为 Serial GC,称老年代回收器为 Serial Old GC,其中新生代采用的是 复制算法,而老年代采用的是 标记整理算法

不过不管是新生代回收器还是老年代回收器,都是基于单线程串行进行回收的,这也就意味着其在多核 CPU 的环境下,只能使用一个 CPU 核心进行回收工作,如果 Java 应用部署在多核环境下,将无法充分利用 CPU 资源造成资源浪费。因此,Serial GC 更适合单核 CPU 或者是小内存空间的场景中使用。

Serial 回收器使用的算法

  • 新生代回收器 Serial 使用的是 复制算法
  • 老年代回收器 Serial Old 使用的是 标记整理算法

Serial 回收器优缺点

◆优点:

  • 简单高效: Serial GC 是一种简单的垃圾回收器,实现简单,回收过程比较稳定;
  • 内存占用少且CPU消耗低: Serial GC 内存占用少,并且使用单线程进行垃圾回收,执行时没有线程交互的开销,CPU 资源消耗低,适合小型 Java 应用程序;

◆缺点:

  • 不支持并行回收: Serial GC 使用的是单线程执行回收,无法充分利用 CPU 资源,执行效率比较低;
  • 回收停顿时间长: 执行回收时会暂停全部用户线程,直至回收结束,停顿时间长,会对应用的性能产生影响;
  • 不适用于大型Java应用: Serial 回收器使用单线程进行回收,回收时也会造成长时间的停顿,影响应用程序的性能,因此不适用于大型 Java 应用程序;

Serial 回收器常配置参数

  • -XX:+UseSerialGC: 启用 Serial 新生代和 Serial Old 老年代回收器。

5.2 ParNew 回收器

ParNew 回收器简介

ParNew GC 是一款并行回收器,除了在进行垃圾回收时使用多线程并发执行外,其它方面几乎和 Serial GC 一致,包括 回收算法Stop The World对象分配规则回收策略 等,因此常称 ParNew GC 为 Serial GC 的多线程版本。不过 ParNew GC 属于新生代回收器,只能对新生代中的对象进行回收,而不能回收老年代中的对象。

还有需要注意的是,ParNew GC 在单核甚至双核环境下,则不一定有 Serial GC 回收效率高,因为线程间切换也是有着不小的损耗。不过,随着应用部署的环境 CPU 核心数量的增加,ParNew GC 相较于 Serial GC 的优势会越来越明显,但并不是成倍增长的,原因还是因为多线程间切换的开销造成的性能损耗。

ParNew 回收器使用的算法

  • ParNew 回收器只能应用于新生代中,使用的是 复制算法

ParNew 回收器优缺点

◆优点:

  • 支持并行回收: ParNew GC 支持多线程并行进行回收,可以利用多核 CPU 的优势,提高垃圾回收的效率;
  • 回收效率高且停顿时间短: ParNew GC 是一个专门用于回收年轻代的垃圾垃圾回收器,使用的是复制算法,并且回收的空间比价小,所以回收效率高且停顿时间短;

◆缺点:

  • 浪费部分内存空间: ParNew GC 使用的是复制算法,需要将内存空间拆成两份,每次只使用其中一份内存空间存储对象,所以比较浪费内存空间;
  • 老年代垃圾回收效率低: 由于 ParNew GC 只用于年轻代垃圾回收,而不处理老年代垃圾回收,因此老年代的垃圾回收效率低下,容易导致 Full GC;

ParNew 回收器常配置参数

  • -XX:+UseParNewGC: 启用新生代回收器 ParNew。
  • -XX:ParallelGCThreads: 配置 GC 的线程数量,通常推荐该值和 CPU 核心数量保持一致。

5.3 Parallel 回收器

Parallel 回收器简介

Parallel GC 和 ParNew GC 比较类似,是一种并行垃圾回收器,其支持多线程并行进行垃圾回收,而且该回收器与其它回收器不同,其关注的重点是吞吐量,所以常说 Parallel GC 是一种注重吞吐量优先的回收器。

在之前介 "评估 GC 的性能指标" 的章节中提及过,应用的吞吐量计算公式为: 吞吐量 = 用户线程运行时间 / (用户线程运行时间 + GC 线程运行时间)。比如,程序运行了 99s,GC 执行耗时 1s,则吞吐量为 99/(99+1)=99%

高吞吐量意味着可以高效率的利用 CPU 时间片,尽快的完成应用程序中的运算任务,因此,Parallel GC 更适合在运算多交互少的场景下执行垃圾回收工作。

Parallel GC 可以分为 Parallel Scavenge 和 Parallel Old 俩种,前者是新生代回收器,使用的是复制算法算法,后者是老年代回收器,使用的是标记整理算法。

并且,新生代回收器 Parallel Scavenge 能够配置自适应调节策略,如果开启了该策略,这时虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整 GC 相关参数,以提供最合适的停顿时间或者最大的吞吐量。比如,可以根据当前系统运行情况,动态调整新生代中 Eden 和 Survivor 区的大小比例,或者是调整新生代中的对象晋升到老年代的岁数等。

Parallel 回收器使用的算法

  • 新生代回收器 Parallel Scavenge 使用的是 复制算法
  • 老年代回收器 Parallel Old 使用的是 标记整理算法

Parallel 回收器优缺点

◆优点:

  • 简单易用: Parallel GC 具有简单的配置和使用方法,无需进行复杂的参数调整,因此适合用于那些不需要过多配置的应用场景;
  • 注重吞吐量 Parallel GC 注重于吞吐量,这使得应用程序在长时间运行时能够保持较高的性能,从而提升用户体验;
  • 支持并行回收: Parallel GC 是一种并行垃圾回收器,能够利用多个 CPU 核心同时进行垃圾回收,从而提高垃圾回收效率,使得应用程序的吞吐量得到提升;

◆缺点:

  • 回收停顿时间长: Parallel GC 执行回收时会暂停全部用户线程,直至回收结束,停顿时间长,会对应用的性能产生影响;
  • 可预测性差: Parallel GC 回收过程中会使用多个 CPU 核心进行并行回收,因此无法在某些情况下精确控制垃圾回收时间,可能会影响应用程序的响应性;
  • 需要较多硬件资源: Parallel GC 执行时会使用到较多的 CPU 核心和内存资源来保证高效的垃圾回收能力,因此如果应用程序在硬件资源较为有限的环境下运行,可能会导致应用程序性能的下降;

Parallel 回收器常配置参数

  • -XX:+UseParallelGC: 启用新生代回收器 Parallel Scavenge。
  • -XX:+UseParallelOldGC: 启用老年代回收器 Parallel Old。
  • -XX:ParallelGCThreads: 配置 GC 的线程数量,通常推荐该值和 CPU 核心数量保持一致。
  • -XX:MaxGCPauseMillis: 配置 GC 回收最大停顿时间,即 Stop The World 时间。
  • -XX:GCTimeRatio: 配置 GC 回收时间占总时间的比例,用于衡量吞吐量的大小,取值范围为 0-100,默认值为 99,也就表示垃圾回收时间不超过 1%。
    • 该参数与 -XX:MaxGCPauseMillis 参数有一定矛盾性,因为设置较短的停顿时间可能会导致更多的 GC 次数,从而增加总的 GC 时间,使得 GCTimeRatio 容易超过设定的比例。
  • -XX:+UseAdaptiveSizePolicy: 配置 Parallel Scavenge 回收器具有自适应调节策略。
    • 在这种模式下,新生代 Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,期望在堆大小、吞吐量和停顿时间之间达到最佳平衡。
    • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMillis),让虚拟机自己完成调优工作。

5.4 CMS 回收器

CMS 回收器简介

CMS GC 诞生于 JDK 5 版本中,它的全称是 Concurrent Mark Sweep Garbage Collector,翻译过来就是 "并发标记清除垃圾回收器",这款回收器是 HotSpot 虚拟机中第一款真正意义上的并发回收器,它第一次实现了让垃圾收集线程与用户线程同时工作。

CMS GC 是一款老年代回收器,其主要回收目标就是老年代中的对象,使用的是标记清除算法。该回收器最大的特点在于它在进行垃圾回收过程中,可以尽可能减少暂停应用程序执行的时间 (Stop The World)。

不过,CMS 回收器相比于其它回收器而言也存在一些缺点,例如 CMS 使用的是标记清除算法,所以在执行完垃圾回收后可能会造成大量且不连续的空间碎片。而且 CMS GC 在执行垃圾回收过程中,大部分执行过程是可以和应用程序并发执行,这会占用一部分本该应用程序使用的 CPU 时间片,这就有可能导致应用程序性能下降。

注: 在 JDK 9 版本中 CMS GC 已经被标记为不推荐使用,并且在 JDK 14 版本中 CMS GC 已经被删除。

CMS 回收器执行步骤

CMS 回收器在执行垃圾回收时一般大致会经历以下几个步骤:

  • 初始标记 (Initial Mark): 在该阶段中,仅仅标记出所有可以作为 GC Roots 的根节点对象,以及这些根节点直接关联的对象。该阶段会触发 STW,造成应用线程短暂停顿。不过由于当前阶段需要标记的对象数量较少,因此这个阶段的暂停时间非常短。
  • 并发标记 (Concurrent Mark): 在该阶段中,主要是从 GC Roots 直接关联的对象开始,逐步向下遍历引用链中全部关联的对象,对其进行标记。该阶段中 GC 线程可以和用户线程并发执行,不会触发 STW 造成应用停顿。
  • 重新标记 (Remark): 在该阶段中,主要是对上一阶段中由于 GC 线程和用户线程并发执行,从而造成有的对象的引用可能发生变动,所以该阶段主要是对这些发生了变动的对象进行重新标记,从而保证标记的准确性。该阶段执行过程中也会触发 STW,造成应用线程停顿,不过由于变动的对象非常少,所以停顿时间不会很长。
  • 并发清除 (Concurrent Sweep): 在该阶段中,主要是并发清除掉所有未被标记的死亡对象,释放内存空间。这个过程 GC 线程可以和用户线程并发执行。

CMS 回收器使用的算法

  • CMS 回收器只能应用于老年代中,使用的是 标记清除算法

CMS 回收器优缺点

◆优点:

  • 支持并行回收: CMS GC 使用多个线程同时执行垃圾收集任务,能够利用多个 CPU 核心同时进行垃圾回收,从而提高垃圾回收效率;
  • 低停顿时间: CMS GC 执行回收过程中 "并发标记" 和 "并发清除" 阶段中,用户线程和 GC 线程可以并发执行,这样可以一定程度上降低整体的 STW (Stop The World) 停顿时间,保证应用响应速度;

◆缺点:

  • 内存碎片化: CMS GC 使用的是标记清除算法,因此每次 GC 执行完成会产生大量不连续的内存空间碎片,可能会造成大对象分配困难,分配大对象时容易触发 Full GC;
  • 影响应用性能: 执行过程中的并发执行阶段中,由于用户线程和 GC 线程并发执行,GC 线程会占用部分 CPU 资源,这会导致应用的吞吐量降低;
  • 会产生浮动垃圾: 执行过程中会产生浮动垃圾,只能在下一次 GC 执行时才能被回收掉,而且如果在清理过程中如果预留给用户线程的内存空间不足时会出现 Concurrent Mode Failure 错误,这时拟机将启动 Serial Old GC,执行 Full GC 回收过程;

在 JDK 9 版本前 CSM GC 可以开启 -XX:+UseCMSCompactAtFullCollection 参数,这样在 CMS 执行完后空间仍然不足时会触发 Full GC,不过在触发 Full GC 前会先进行一次内存整理,不过在 JDK9 版本中该选项已经废弃;

CMS 回收器可配置参数

  • -XX:+UseConMarkSweepGC: 启用 CMS 老年代回收器。
  • -XX:ParallelcMSThreads: 设置 CMS GC 执行时的线程数量。
  • -XX:CMSInitiatingOccupanyFraction: 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
  • -XX:+UseCMSCompactAtFullCollection: 设置是否在执行完 Full GC 前对内存空间进行压缩整理,以此避免内存碎片的产生。
  • -XX:CMSFullGCsBeforeCompaction: 设置在执行多少次 Full GC 后对内存空间进行压缩整理。

5.5 G1 回收器

G1 回收器简介

G1 (Garbage First) GC 是一种新一代的垃圾回收器,最早在 JDK 6 Update 14 中作为实验性功能加入,并在 JDK 7 Update 4 版本中正式引入,并在 JDK 9 版本中成为默认的垃圾回收器。

G1 回收器使用了一种名为 Region (区域) 的概念,将整个堆空间在逻辑上划分为多个大小相等的 Region。每个 Region 都是连续的一段内存区域,大小在 1MB ~ 32MB 之间,且为 2 的 N 次幂,具体大小根据堆的实际大小而定。并且每个 Region 扮演着不同的角色,比如可以是 Eden 区、Survivor 区、Old 区或 Humongous 区等,这些区域可以被垃圾回收器进行精细化管理和回收,从而避免了全局垃圾回收带来的长时间停顿。

G1 回收器也是一个既注重吞吐量,同时也兼顾重停顿时间的回收器。其采用了分代垃圾回收的思想,同时也利用了可预测的停顿时间来保证垃圾回收的效率。

G1 回收器与其它垃圾回收器不同的是,其并不是按照固定的分代比例来进行垃圾回收的,而是根据应用程序的实际运行情况来动态地调整垃圾回收策略,这使得 G1 回收器的停顿时间是可预测的,可以在可控制的停顿时间内进行垃圾回收,从而避免了应用程序出现长时间的停顿,提高了应用程序的响应速度。

除此之外,G1 垃圾收集器提供了一系列的配置参数,允许用户根据应用程序的实际情况调整垃圾回收器的性能表现。同时,G1 垃圾收集器也支持并发处理,可以在多个 CPU 核心上并行执行垃圾回收任务,提高了垃圾回收器的并发性能。

G1 回收器执行步骤

在 G1 中支持三种回收模式,分别为 Young GCMixed GCFull GC:

  • Young GC: 对新生代中的 Region 进行回收。当新生代的空间不足时会触发 Young GC,回收新生代中的垃圾对象。
  • Mixed GC: 这是一种混合回收模式,不仅回收新生代中的 Region,还会回收部分老年代中的 Region。这种模式旨在减少 Full GC 的频率,通过定期回收部分老年代 Region 来降低整个堆的垃圾积累。
  • Full GC: 对整个堆空间的所有 Region 进行回收,包括新生代和老年代。Full GC 通常在 G1 无法通过 Mixed GC 清理足够的空间时触发,或者在某些特殊情况下触发 (如系调用 system.gc() 方法触发)。

其中的 Young GCFull GC 与其它垃圾回收器类似,但 Mixed GC 是 G1 特有的回收模式,通过这种方式 G1 能够更好地平衡垃圾回收的效率和停顿时间。而这里要介绍的也是 G1 中的 Mixed GC 执行过程,如下:

  • 初始标记 (Initial Mark): 在该阶段中,仅仅标记出所有可以作为 GC Roots 的根节点对象,以及这些根节点直接关联的对象。这个阶段会触发 STW 造成应用线程短暂停顿。不过由于当前阶段需要标记的对象数量较少,因此这个阶段的暂停时间非常短。
  • 根区域扫描 (Root Region Scanning): 这个阶段执行时,会扫描上一步标记出的根区域里面的对象,寻找这些对象指向的其它区域。这一阶段在执行过程中,不会触发 STW 造成应用线程停顿。
  • 并发标记 (Concurrent Mark): 这个阶段执行时,G1 会从上一阶段找到的根开始遍历整个对象图,标记出所有活动对象。这个过程是在多个线程上并行执行的,也就是说这个阶段中 GC 线程和应用线程可以并行执行。
  • 重新标记 (Remark): 这个阶段的主要目的就是,处理并修正因并发标记期间由于应用程序线程继续执行而造成的对象图的变化。这个阶段会触发 STW 造成应用线程短暂停顿。
  • 筛选回收 (Live Data Counting): 在最终标记结束后,G1 GC 会执行一次筛选回收,通过计算每个 Region 中存活对象的数量和可用空间,来确定哪些 Region 需要被回收。一般情况下,G1 GC 会选择包含垃圾最多的 Region 作为下一个要回收的目标 Region,这些 Region 被称为 "回收集" (Collection Set, CSet)。然后,G1 GC 会清理 CSet 中的垃圾对象,将存活的对象复制到新的区域中。这个过程执行时,也会出发 STW 造成应用线程停顿。
  • 混合回收 (Mixed Collection): 当系统运行一段时间后,不仅年轻代需要回收,老年代中也会积累一定数量的垃圾。此时,G1 GC 会在常规的年轻代回收周期中加入一部分老年代的区域,进行混合回收。这样做的好处是可以避免长时间的老年代 GC,提高系统的响应速度。
  • 清理 (Cleanup): 在混合回收结束之后,G1 GC 会对回收后的区域进行整理,释放空闲空间,更新指针等。

总之,G1 回收器通过 "初始标记"、"根区域扫描"、"并发标记"、"重新标记"、"筛选回收"、"混合回收" 和 "清理" 等几个阶段来进行垃圾回收。通过这样的回收流程,使得 G1 能够实现可预测的停顿时间、高效的内存回收和优化的内存分配,成为当前推荐使用的垃圾回收器之一。

G1 回收器使用的算法

  • G1 回收器整体使用的是 标记-整理算法,局部使用的是 复制算法

G1 回收器优缺点

◆优点:

  • 可预测的停顿时间: G1 GC 将堆空间分成多个大小相等的 Region (区域),这些 Region 可以作为 Eden 区、Survivor 区或 Old 区等。通过在多个 Region 中执行并发垃圾收集,G1 能够实现可预测的停顿时间,这意味着垃圾收集的停顿时间可以预测和控制;
  • 高效的内存回收: G1 GC 使用增量式的垃圾收集算法,不仅在特定区域中进行垃圾回收,还在整个堆中进行。这种全局性的垃圾回收方式使得 G1 能够高效地回收内存,并减少垃圾收集的停顿时间;
  • 优化的内存分配: G1 GC 使用了一种名为 Remembered Set 的数据结构,可以追踪对象之间的引用,从而在内存分配时避免扫描整个堆。这一特性可以显著减少内存分配的时间;
  • 空间整理: G1 GC 通过对未使用的 Region 进行空间整理,将零散的空闲空间合并成更大的连续空间块,从而优化内存使用,提高堆的利用率;

◆缺点:

  • 对硬件资源要求较高: G1 GC 需要在多核 CPU 和大内存环境下运行才能充分发挥其优势。如果在较小的硬件资源上运行,可能会导致运行缓慢或 OutOfMemoryError 等问题;
  • 初始标记和最终标记的时间较长: G1 GC 的初始标记和最终标记阶段需要单线程执行,并且会暂停应用程序。在处理大型内存空间时,这两个阶段可能会导致较长的停顿时间;
  • 混合收集过程可能会影响吞吐量: G1 GC 的混合收集过程涉及多次暂停应用程序,这可能会对应用程序的吞吐量产生一定的影响;

六、如何选择回收器

6.1 不同类型回收器汇总

每一款的垃圾收集器都有不同的特点,在具体使用的时候需要根据具体的情况选用不同的垃圾收集器,常用的回收器汇总如下:

垃圾收集器 分类 作用位置 使用算法 特点 适用场景
Serial 串行运行 新生代 复制算法 响应速度优先

适用于单 CPU 环境下的 Client 模式

ParNew 并行运行 新生代 复制算法 响应速度优先

多 CPU 环境 Server 模式下与 CMS 配合使用

Parallel Scavenge 并行运行 新生代 复制算法 吞吐量优先

适用于后台运算而不需要太多交互的场景

Serial Old 串行运行 老年代 标记整理算法 响应速度优先

适用于单 CPU 环境下的 Client 模式

Parallel Old 并行运行 老年代 标记整理算法 吞吐量优先

适用于后台运算而不需要太多交互的场景

CMS 并发运行 老年代 标记清除算法 响应速度优先

适用于互联网或 B/S 业务

G1 并发、并行运行 新生代 & 老年代 标记整理算法/复制算法 响应速度优先

面向服务端应用

6.2 选择合适的回收器

针对不同场景应该选择不同类型的回收器,建议如下几条:

  • 小内存空间 (如小于100M) 或单核场景下: 建议选择串行执行的 Serial 回收器;
  • 注重吞吐量且多核心 CPU 场景,内存空间有限: 建议选择并行执行的 Parallel 回收器;
  • 注重停顿时间且多核心 CPU 场景,内存空间有限: 建议选择并发执行的 CMS 回收器;
  • 对吞吐量和停顿时间都有要求,多核心 CPU 场景,内存空间非常大: 建议选择并发执行的 G1 回收器;

以上建议仅供参考,实际情况需具体分析。在现代互联网应用场景中,机器配置通常较好,因此多数情况下推荐使用 G1 回收器。

七、垃圾回收相关问题

7.1 Java 中常用的垃圾回收器都有哪些?

  • 常用的新生代垃圾回收器: Serial、ParNew、Parallel Scavenge;
  • 常用的老年代垃圾回收器: Serial Old、Parallel Old、CMS;
  • 既能新生代又能老年代的垃圾回收器: G1;

7.2 Java 1.8 中默认的垃圾回收器?

  • 默认的新生代垃圾回收器是 Parallel Scavenge;
  • 默认的老年代垃圾回收器是 Parallel Old;

7.3 CMS 垃圾回收器会发生 Stop The World 吗?

CMS 中的 "初始标记阶段" 和 "重新标记阶段" 都会发生短暂的 Stop The World,造成应用线程停顿。

7.4 CMS 和 G1 垃圾回收器有什么不同?

  • CMS 是一个注重停顿时间的垃圾回收器,使用的是标记清除算法,主要用于回收老年代中的对象。而且,该回收器可以和 Serial、ParNew 等新生代垃圾回收器配合使用。不过由于使用标记清除算法,执行回收后可能会产生大量的内存碎片,影响内存的连续性和分配效率。
  • G1 是一个 既兼顾停顿时间又注重吞吐量的垃圾回收器,使用的是标记整理算法和复制算法。而且该回收器既能回收新生代中的对象,也能回收老年代中的对象。由于使用标记整理算法,执行垃圾回收后不会产生内存碎片,有助于提高内存的连续性和分配效率。

---END---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。