一、概览

目前在 Hotspot VM 中主要有分代收集和分区收集两大类,具体可以看下面的这个图

495132b11ec5e023d353d8e964626a01557602.jpg

分代收集器

  • ParNew: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
  • CMS: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除,详情可见 JEP 363。


分区收集器

  • G1: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
  • ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
    Shenandoah: 由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近,下图为与 CMS 和 G1 等收集器的 benchmark。

c118d46c63363c221b4034c4ce05099381552.jpg

常用收集器
目前使用最多的是 CMS 和 G1 收集器,二者都有分代的概念,主要内存结构如下:

3a6dacdd87bfbec847d33d09dbe6226d199915.png


以上仅列出常见收集器,除此之外还有很多,如 Metronome、Stopless、Staccato、Chicken、Clover 等实时回收器,Sapphire、Compressor、Pauseless 等并发复制/整理回收器,Doligez-Leroy-Conthier 等标记整理回收器,由于篇幅原因,不在此一一介绍。


二、收集器介绍

概览

评判 GC 的两个核心指标[6]:

  • 延迟(Latency): 也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大,GC 技术的主要发展方向。
  • 吞吐量(Throughput): 应用系统的生命周期内,由于 GC 线程会占用 Mutator(应用服务) 当前可用的 CPU 时钟周期,吞吐量即为 Mutator 有效花费的时间占系统总运行时间的百分比,例如系统运行了 100 min,GC 耗时 1 min,则系统吞吐量为 99%,吞吐量优先的收集器可以接受较长的停顿。

机制 优缺点 适用场景
Serial 收集器 1、单线程STW收集
2、新生代复制算法,老年代标记整理算法
优:无线程交互,简单
缺:线程资源浪费、延迟高
适应于只有单核的场景,基本弃用,可与CMS配合
Parallel收集器 1、多线程STW收集
2、新生代复制算法,老年代标记整理算法

适用于注重吞吐量以及CPU资源的场景(JDK8默认)
ParNew 收集器 1、多线程STW收集
2、新生代复制算法,老年代标记整理算法
优: 在Server模式下的虚拟机,和CMS配合
CMS 收集器 1、初始(STW)、并发、重新标记(STW)、并发清除
2、老年代-标记清除算法
优:并发收集,延迟低
缺:
1、会和CPU抢资源、
2、浮动垃圾
3、空间碎片
适合对响应时间时间有要求的
G1收集器 主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征 要求尽可能可控 GC 停顿时间;内存占用较大的应用

● 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
● 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。


Serial 收集器

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
image.png
(参考1 3.5.1)


优缺点:
简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。

虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。


Serial Old 收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。


Parallel收集器

4.3 Parallel Scavenge 收集器

-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?

使用 Parallel 收集器+ 老年代串行
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用标记-复制算法,老年代采用标记-整理算法。

这是 JDK1.8 默认收集器
使用 java -XX:+PrintCommandLineFlags -version 命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version “1.8.0_211”
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能

4.5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。


4.2 ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

4.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

image.png

优点:并发收集、低停顿。
缺点:对 CPU 资源敏感、无法处理浮动垃圾、它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

4.7 G1 收集器


G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:

初始标记
并发标记
最终标记
筛选回收


G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

参考
22 | G1 GC:分区回收算法说的是什么?-极客时间

4.8 ZGC 收集器

与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
在 ZGC 中出现 Stop The World 的情况会更少!
详情可以看 :
《新一代垃圾回收器 ZGC 的探索与实践》


三、收集器工作过程

四、收集器选型

image.png
图 收集器配合2

1、ParNew + CMS

ParNew :
1、是Serial收集器的多线程版本,也使用复制算法
2、垃圾收集过程中同样也要暂停所有其他的工作线程(缺点)
3、默认开启和CPU数目相同的线程数
4、在Server模式下新生代的默认垃圾收集器
CMS:
特点:
1、是一种年老代垃圾收集器
2、最主要目标是获取最短垃圾回收停顿时间
3、是Sun HotSpot虚拟机中第一款真正意义上并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时工作
4、最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验,高速响应
5、使用多线程的标记-清除算法
缺点:
1、对CPU资源敏感,在多CPU下才能体现优势;虽然不会导致用户线程停顿,但是占用CPU导致应用程序变慢,引起吞吐量下降;
2、永久代空间(或JDK8的元空间)耗尽,默认情况下,CMS不会对永久代进行收集,一旦永久代空间耗尽,就回触发Full GC;
3、无法处理浮动垃圾,CMS在并发清理阶段用户线程依然在运行,并产生新垃圾(浮动垃圾),本次无法清除,需待下次GC清除;CMS的预留空间(-XX:CMSInitiatingOccupancyFraction设置预留空间占比)不够用户线程使用,会出现“Concurrent Mode Failure”失败,此时JVM会临时启用Serial Old收集器来重新进行老年代的垃圾收集,导致停顿时间变长,性能反而降低;
4、会产生大量内存空间碎片,大量内存碎片可能实际老年代空间很多,但不够大块对象使用,导致提前触发FGC;虽然CMS提供了碎片空间整理压缩机制,但是整理过程不是并发,导致停顿变长;
对于CMS:-XX:ParallelCMSThreads:设定CMS的线程数量
适用场景:重视服务器响应速度,要求系统停顿时间最短。
但是有个明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决。所以,对于高并发系统,CMS也并非最优。

2、G1

特点:
1、JDK7引入,JDK8及以上成熟,JDK9默认收集器;
2、基于标记-整理算法,不产生内存碎片;
3、可以非常精确控制停顿时间,在不牺牲吐量前提下,实现低停顿垃圾回收;
4、G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域;
缺点:
1、G1内部有个region区域块的概念, 它的大小和大对象很难保证一致,这会导致空间的浪费;特别大的对象是可能占用超过一个 region 的。如果设置region大小不合理,会导致大对象分配空间时内存空间地址不连续。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。

3、Parallel Scavenge+Parallel Old

Parallel Scavenge:
1、是一个新生代垃圾收集器,同样使用复制算法
2、是一个多线程的垃圾收集器
3、它重点关注的是程序达到一个可控制的吞吐量
4、主要适用于在后台运算而不需要太多交互的任务
5、自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别
缺点:
1、回收的时间变短了,但是回收的次数会增多,这一点在促销系统实践上非常明显。
Parallel Old:
1、是Parallel Scavenge的年老代版本
2、使用多线程的标记-整理算法
3、JDK7、JDK8 默认使用该收集器作为老年代收集器
缺点:
1、停顿时间长
应用场景:在Server模式,多CPU的情况下,如果系统对吞吐量要求比较高,可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。

4、Serial+Serial old

-XX:+UseSerialGC -XX:+UseSerialOldGC


参考

1、《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》
2、https://plumbr.io/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep
3、https://my.oschina.net/hosee/blog/644618
4、https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
5、JVM调优之垃圾回收器选择 - 晓等 - 博客园
6、Java中9种常见的CMS GC问题分析与解决