JVM 中的垃圾收集器

1. 经典垃圾收集器

1.1 Serial 收集器

Serial 收集器是一个新生代采用复制算法的单线程收集器。它的 “单线程” 的意义不仅仅是说明它只会使用一个处理器或者一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

1.2 ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本。

也就是说 ParNew 收集器是一个新生代、采用复制算法、多线程收集器。

ParNew 收集器可以使用多个垃圾收集线程去完成垃圾收集工作,但是在进行垃圾回收的时候同样也要 Stop The World,直到它收集结束。

1.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器也是新生代、基于标记-复制算法、多线程收集器。

Parallel Scavenge 收集器的很多特性表面上与 ParNew 收集器非常相似,但是Parallel Scavenge 收集器的特点是它的关注点与其他的收集器不同。

CMS 等其他收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。(吞吐量: 是CPU中用于运行用户代码的时间 与 CPU总消耗时间的比值。)

低停顿时间: 适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验。

高吞吐量: 高吞吐量可以最高效率的利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

1.4 Serial Old 收集器

Serial Old 收集器 是 Serial 收集器的老年代版本。

Serial Old 收集器是老年代、基于标记-整理算法、单线程收集器。

1.5 Parallel Old 收集器

Parallel Old 收集器 是 Parallel 收集器的老年代版本。

Parallel Old 收集器 老年代、基于标记-整理、多线程收集器。

1.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适用于需要响应速度快、给用户良好交互体验的应用。

CMS 收集器是 老年代、基于标记-清除、并发收集器。

(这个并发收集器说的是 垃圾收集线程 可以和 用户线程 并发执行。 而前面说的多线程收集器指的是 多个垃圾收集线程之间可以并发执行。)

CMS 收集器的工作过程主要分为四个步骤:

  1. 初始标记: 需要 Stop The Word;但是初始标记仅仅是标记一下 GC Roots 能直接关联到的对象,速度很快。
  2. 并发标记: 并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时比较长,但是不需要 Stop The Word,用户线程可以和垃圾收集线程一起并发运行。
  3. 重新标记: 重新标记阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。需要 Stop The Word,停顿时间比初始标记稍长一些,但远比并发标记阶段的时间短。
  4. 并发清除: 并发清除阶段是清理删除掉标记阶段判断已经死亡的对象,由于是基于标记-清理算法而不是标记-整理算法,所以不需要移动存活对象,也就是不需要 Stop The Word,用户线程可以和垃圾收集线程一起并发运行。

CMS 的优点:并发收集,停顿时间短。

CMS 的 3 个明显的缺点:

  1. 对处理器资源敏感; 因为并发标记和并发清理阶段是和用户线程一起运行,当 CPU(处理器) 数量变少时,CMS 对用户程序会造成很大影响。
  2. 无法处理浮动垃圾,有可能出现 “并发失败” (Concurrent Mode Failure) 而导致另一次完全的 Stop The Word 的 Full GC 产生。
  3. 会产生大量内存碎片; 因为 CMS 是基于标记-清除算法。

关于第 2 个缺点的详细说明:

浮动垃圾:并发标记和并发清理阶段,用户线程还在运行,可能会产生新的垃圾,这些垃圾就是浮动垃圾,这些垃圾需要在下次垃圾回收周期时才能回收掉。

由于垃圾收集阶段用户线程还需要持续运行,那就还要预留足够内存空间提供给用户线程使用;因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满再进进行收集,它必须预留一部分空间供并发收集时的程序运行使用。 通过参数**-XX:CMSInitiatingOccupancyFraction** 的值可以来控制内存使用百分比。

适当提高内存使用百分比可以降低内存回收频率,获取更好的性能。但是如果内存使用百分比设置的太高,预留的内存可能无法满足程序分配新对象的需要,就会出现一次 “并发失败” (Concurrent Mode Failure),那么虚拟机就回启动备用策略:冻结用户线程的执行(Stop The Word),临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。这样的话,停顿时间就会大幅增加。所有内存使用百分比设置的抬高容易造成并发失败,反而降低性能。

1.7 G1 收集器

G1 (Garbage-First)是一款主要面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.

G1 (Garbage-First)是一款主要面向服务器的垃圾收集器,它不是将 Java 堆划分成新生代和老年代,而是将 Java 堆划分为多个大小相等的独立的区域(Region),每一个区域都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间 或者老年代空间。对于扮演不同角色的区域,采用不同的策略去处理。(比如对于扮演新时代的区域采用标记-复制算法,对于扮演老年代的区域采用标记-整理算法)。

通过将 Java 堆划分为一个个的区域,G1 收集器可以避免在 Java 堆中进行全区域的垃圾收集;G1收集器跟踪各个区域里面的垃圾堆积的价值大小(价值即回收所获得的空间大小以及回收所需时间的经验值),然后在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的区域。

G1 收集器工作过程大致可以为 4 个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 帅选回收: 首先对各个Region的回收价值和成本进行计算,然后根据用户期望的GC停顿时间来制定回收计划。

G1 收集器的特点:

  1. 并行与并发:G1能充分利用多CPU,多核环境下的硬件优势,来缩短Stop the World,是并发的收集器。
  2. 分代收集:G1不需要其他收集器就能独立管理整个GC堆,保留了分代的概念。
  3. 空间整合:G1从整体来看是基于标记-整理算法,从局部(两个Region)上看基于复制算法实现,G1运作期间不会产生内存空间碎片。
  4. 可预测的停顿:能够建立可以预测的停顿时间模型,可以由用户指定期望的停顿时间(当然,这个期望停顿时间不能低的太过离谱,不然只会适得其反)。

CMS 收集器和 G1 收集器的对比:

  1. CMS 和 G1 的关注点都在于停顿时间,但 G1 可以指定最大停顿时间。

  2. CMS 是按新生代和老年代划分内存;G1 是按区域划分内存,并且 按受益动态确定回收集。

  3. CMS 是基于 标记-清除 算***产生内存碎片。
    G1 从整体上看是基于 标记-整理 算法,局部上看是基于 标记-复制 算法,不管是整体上还是局部,G1 都不会产生内存碎片。

  4. G1 为了垃圾收集产生的内存占用 以及 程序运行时的额外执行负载 都要高于CMS。

  5. 小内存应用上 CMS 表现优于 G1;(平衡点通常在 6 - 8 G内存之间)
    大内存应用上 G1 表现优于 CMS;

    但是这个表现并不绝对,具体应用需要具体的测试和比较才能得知。

2. 其他收集器

2.1 ZGC 收集器

ZGC 是在 JDK11 中新加入的具有实验性质的低延迟垃圾收集器,目标是在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把停顿时间限制在 10 毫秒以内的低延迟。

ZGC 是基于 Region 内存布局,ZGC 的 Region 具有动态性—可以动态创建和销毁,并且区域容量的大小也是动态的。

ZGC 不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理。

当然,还有一些其他收集器,比如:Shenandoah 收集器、Epsilon 收集器 等。有兴趣的可以去了解一下,限于篇幅这里就不介绍了。

3. 垃圾收集器的选择

垃圾收集器的选择一般考虑 3 个因素:

1.应用程序的关注点是什么?

  • 是关注高吞吐量?比如数据分析类、科学计算类任务,目标是尽快计算出结果。
    还是关注低延迟?比如服务供应应用,目标是用户交互体验。
  • 还是关注垃圾收集的内存占用?比如客户端应用或嵌入式应用,目标是不能占用太多内存。

2.应用程序的基础设施条件是什么?

  • 硬件规格是什么?
  • 系统架构是什么?
  • 处理器数量是多少?
  • 操作系统是什么?

3.使用的 JDK 发行商和版本号是什么?

  • 不同发行商和版本所支持的垃圾收集器不同。

注: 如果猿兄这篇博客有任何错误和建议,欢迎大家留言交流!

如果这篇文章📝对你有用,记得点个赞👍 关注一下呀!💗