vlambda博客
学习文章列表

JVM系列之经典垃圾回收器(上篇)

封面图

1.前言

随着 JDK 的不断更新,垃圾回收器的效率也越来越高。每一次 JDK 的更新,必然会包含有垃圾回收器的更新,截止目前,在最新的 JDK14 版本中,最新的垃圾回收器为 ZGC。

从垃圾回收器发展至今,出现过很多垃圾回收器,例如:Serial、ParNew、Parallel Scavenge、SerialOld、CMS、Parallel Old、G1、Shenandoah、ZGC 等,虽然目前比较流行的是 G1 和 ZGC,但是那些经典的垃圾回收器我们也有必要了解一下它们的工作原理,一方面是因为目前仍然有很多系统使用的都是 JDK8 及以下版本,而这些版本中有很多系统都是默认使用的经典的垃圾回收器,搞懂它们的原理,方便我们对它进行调优,另一方面就是为了面试,毕竟垃圾回收器是面试高频考点。

接下来本文将先介绍 Serial、ParNew、Parallel Scavenge、SerialOld、CMS、Parallel Old 这六款经典的垃圾回收器的工作原理,以及使用场景和相关参数配置,在下一篇文章中将会主要介绍 G1、Shenandoah、ZGC 这些 GC 的工作原理。

2.性能指标

在介绍垃圾回收器之前,先介绍一下衡量垃圾回收器的几个常用指标。

首先是「吞吐量」,它描述的是用户线程执行的时间比上全部运行时间(全部运行时间 = 用户线程时间+垃圾回收线程执行时间执行),吞吐量越高,表明系统的资源利用率越高。

然后是「停顿时间」,它表示的是 GC 线程在执行过程中,导致用户线程停顿的时间,如果停顿时间越长,那么用户线程卡顿的时间越长,用户体验越差,因此我们希望的是停顿时间越短越好。

另外还有一个指标就是「内存占用率」,因为垃圾回收器在执行过程中,它也需要占用一定的内存空间,当然我们期望的是内存占用率越小越好,尤其是在服务器内存配置较低的情况下。如果服务器的资源配置很高,内存很大,内存占用率高一点也可以接受。

通常情况下,「吞吐量和低延时(停顿时间)这两个指标是对立的」,无法同时兼顾两者,如果想追求低延时,那么吞吐量就会下降;如果追求高吞吐量,那么停顿时间就会变长。不过随着目前垃圾回收器的不断发展,越来越多的垃圾回收器都是以「在保证高吞吐量的情况下,尽可能的去追求低延时」为原则,来进行垃圾回收器的实现。

3.Serial

Serial 是针对新生代的垃圾回收器,它是单线程执行的,是一款串行的垃圾回收器,采用的是复制算法。它的单线程并不仅仅指它在进行垃圾回收时是单线程或者单处理器执行,更深的含义是它在垃圾回收时,需要暂停其他所有的线程,造成 STW。

当 JVM 处于客户端模式下时,Serial 是默认的垃圾回收器,它的优点是简单高效。在内存资源受限的环境下,Serial 垃圾回收器相比其他垃圾回收器,它所占用的内存更小。对于单处理器的场景,Serial 处理器由于是单线程的,它省去了线程之间的资源竞争,因此会更加高效。

当使用参数 「-XX:+UseSerialGC」时,在开启使用Serial垃圾回收器同时,老年代的垃圾回收器为Serial Old。

4.Serial Old

和 Serial 一样,Serial Old 也是单线程执行的,是一款串行的垃圾回收器,不同的是 Serail Old 回收的是老年代区域,采用的算法是标记-压缩(整理)算法。在进行垃圾回收时,同样也会造成 STW 的现象。

当 JVM 处于「客户端」模式下时,Serial Old 通常与 Serial 垃圾收集器搭配使用,Serial 回收新生代区域,Serial Old 回收老年代区域。Serial 和 Serial Old 搭配使用时的示意图如下。

JVM系列之经典垃圾回收器(上篇)
Serial/Serial Old 搭配进行垃圾回收示意图

当 JVM 处于「服务端」模式下时,Serial Old 有两个用途,其一:与 Parallel Scavenge 垃圾回收器(回收新生代)搭配使用;其二:作为 CMS 垃圾回收器的后备方案(这一点会在下面讲解 CMS 垃圾回收器时具体讲解)。其实 Serial Old 还可以与 ParNew 垃圾回收器搭配使用,不过这种组合方式,从 JDK9 开始,已经被移除了。

5.ParNew

ParNew 是一款针对新生代区域的垃圾回收器,它是 Serial 垃圾收集器的多线程版本,即它是一款并行的垃圾回收器,支持多个垃圾回收线程同时并行回收垃圾,使用的也是复制算法。ParNew 的大部分参数配置和 Serial 收集器一样,但额外多了部分参数,如:可以通过参数 「-XX:ParallelGCThreads」 来指定并行的垃圾回收的线程个数,默认情况下,垃圾回收线程的个数与处理器的个数相等。在单处理器的系统中,ParNew 的性能并不一定比 Serial 好,因为线程的切换需要额外耗费 CPU 资源。

可以使用参数 「-XX:+UseParNewGC」 来开启使用 ParNew 进行垃圾回收。

ParNew 可以和 Serial Old 或者 CMS 搭配使用,然而从 JDK9 开始,官方已经移除了 ParNew 和 Serial Old 的组合使用方式,同时 JDK9 中将 CMS 标记为 Deprecated 状态,在 JDK14 中彻底移除 CMS,这就导致了 ParNew 将处于一个十分尴尬的地位,在高版本中既不能和 Serial Old 搭配使用,也将在未来无法和 CMS 搭配使用,这就导致了 ParNew 这款垃圾回收器必然消失在历史的舞台。

JVM系列之经典垃圾回收器(上篇)
ParNew/Serial Old 搭配进行垃圾回收示意图

6.Parallel Scavenge

Parallel Scavenge 也是一款针对「新生代的并行的」垃圾回收器,它和 ParNew 虽然都是并行、针对新生代,但是它们的区别很大,Parallel Scavenge 是一款「吞吐量优先」的垃圾回收器。适用于那些期望尽可能的利用 CPU 资源、尽快完成程序的运算任务以及不太注重用户交互行为的场景。

Parallel Scavenge 提供了两个参数来精准地控制吞吐量,分别是 「MaxGCPauseMillis」 和 「GCTimeRatio」。

MaxGCPauseMillis 表示的是每次进行 GC 时,系统的最大停顿时间,如果配置了该参数,那么 JVM 在每次进行垃圾回收时,它会尽可能的将停顿时间控制在 MaxGCPauseMillis 之内。该参数并不是配置的越小越好,如果配置得很小,那么 JVM 可能会为了达到停顿时间控制在 MaxGCPauseMillis 之内的目的,选择以减小新生代区域的大小为代价,毕竟每次回收 300M 的空间所花的时间肯定比 500M 的短。「而 JVM 将新生代的内存区域调小后,带来的后果就是垃圾回收进行得更加频繁了,最后会导致系统的吞吐量下降」。通常情况下,我们无法精准地把控每次垃圾回收需要停顿的时间,所以该参数需要慎用,一不小心,配置的不合理,可能适得其反。

GCTimeRatio 表示的是每次 GC 的时间占用的比率是多少(具体计算方是:GCTimeRatio = 用户线程运行时间/ GC 线程运行时间),例如:如果 GCTimeRatio 参数的值配置的 19,那么 GC 运行的时间占总时间的 5%(1/(1+19))。JVM 通过这个参数来达到控制系统吞吐量的目的。

另外 JVM 还提供了一个参数,叫做「UseAdpativeSizePolicy」,它表示的是让 JVM「根据系统的运行情况来动态调整」新生代(Eden、S0、S1)、老年代的大小,我们只需要设置好最基本的内存参数以及 MaxGCPauseMillis(最大停顿时间)或者 GCTimeRatio(目标吞吐量)即可,不需要设置-XX:Xmn(新生代的内存大小)、-XX:SurvivorRatio (Surivivior区域的比例)等参数了,JVM 会根据系统运行时监控到相关信息,来动态进行调整。Parallel Scavenge 支持动态调整策略,这也算是它和 ParNew 收集器的另一大不同之处了。

7.Parallel Old

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,也是支持多线程的并行执行,它底层是基于标记-压缩(整理)算法来实现的。在 JDK6 中才开始停供,在 Parallel Old 出现之前,Parallel Scavenge 收集器只能配合着 Serial Old 使用,无法与 CMS 垃圾回收器配合使用,这是因为 Parallel Scavenge 与 CMS、Serial、ParNew 这些收集器的底层框架不一样,无法兼容导致的。而 Serial Old 又是单线程的垃圾收集器,在多处理器的场景下,性能不高,白白浪费了 Parallel Scavenge 并行的优点,好车配劣马,所以在 Parallel Old 出现之前,Parallel Scavenge 一直处于比较鸡肋的地位。目前,Parallel Scavenge 和 Parallel Old 的组合,其垃圾回收效果不错,是 JDK8 中默认的垃圾回收组合方式。

JVM系列之经典垃圾回收器(上篇)
Parallel Scavenge/Parallel Old 垃圾回收示意图

8.CMS

CMS 的全称是 Concurrent-Mark-Sweep 的缩写,翻译过来就是并发标记清除,它是一款「以低停顿时间为目标」的垃圾回收器,特点是低延时。CMS 的工作原理大致分为四个步骤:初始标记、并发标记、重新标记、并发清除。使用参数:「-XX:+UseConcMarkSweepGC」 即可开启使用 CMS 垃圾回收器。

  1. 「初始标记」指的是仅仅只标记出和 GC Roots 直接关联的对象,这个过程需要暂停所有的用户线程,因此会产生 STW。由于这一步仅仅标记和 GC Roots 直接关联的对象,因此这一步耗费的时间会很短,造成的停顿时间会很短。
  2. 「并发标记」。这一步是从和 GC Roots 直接关联的对象出发,开始遍历整个对象图引用链,这个过程是 GC 线程和用户线程并发执行的,因此不会造成 STW。这一步因为需要遍历所有对象的引用链,所以耗费时间较长,由于不会造成 STW,即使耗时较长,也没有关系。
  3. 「重新标记」。在并发标记阶段,用户线程仍然在运行,因此会改变对象之间的引用关系,那么在重新标记阶段,就是对并发标记的结果进行修正。把那些怀疑是垃圾,而实际不是垃圾的对象重新标记为存活对象。这一步需要暂停所有的用户线程,因此会造成 STW 的现象,这一步的耗时会比初始标记阶段长一些,但是远小于并发标记阶段的耗时。
  4. 「并发清除」。这一阶段是垃圾回收线程和用户线程一起并发执行,垃圾回收线程进行垃圾对象的清除,这一步耗时较长,但不会造成 STW。

整体上来看,CMS 垃圾回收器只有在初始标记阶段和重新标记阶段会造成用户线程的停顿,但是这两步都耗时较短,因此整体上,CMS 进行垃圾回收时,是低延时的。示意图如下。

CMS 垃圾回收示意图

8.1 CMS 优缺点与参数调优

CMS 垃圾回收器的优点就是低延时,对于那些期望快速响应、暂停时间较短以提高用户体验的网站或者系统,CMS 垃圾回收器就特别适合它们。

CMS 的缺点也很明显。第一,「对 CPU 资源比较敏感」。因为垃圾回收线程需要和用户线程并发执行,会涉及到争夺 CPU 资源的现象,导致系统的吞吐量下降。我们可以通过参数 「-XX:ParallelGCThreads」 来控制垃圾回收线程的数量,系统默认的数值是 「(处理器核心数+3)/4」。

第二,CMS 「会产生"浮动"垃圾」。在 CMS 进行垃圾回收时,用户线程也在执行,在此过程中,用户线程可能也会产生新的垃圾,而这些新的垃圾在 CMS 进行本次垃圾回收时,是不会被回收掉的,这些垃圾被称之为"浮动"垃圾。也正是因为垃圾线程和用户线程存在同时执行的场景,因此 CMS 垃圾回收器,不会等到堆内存被使用完才进行垃圾回收,它需要为系统预留一部分内存以供用户线程运行,所以 CMS 通常是当堆内存使用率达到达到某一个阈值之后,就会触发执行垃圾回收。

该阈值在 JDK5 中,默认值为 68%,到了 JDK6 以后,该阈值的默认值被提高到了 92%,我们可以通过参数 「-XX:CMSInitiatingOccupancyFraction」 手动设置。在实际使用过程中,如果系统的内存使用增长比较慢,那么这个阈值就可以适当的设置得大一点,降低垃圾回收的频率,以提高系统性能。但如果设置得过大,也会带来额外的问题,当设置得过大时,也即是内存即将被使用完,此时由于用户线程仍在运行,如果恰好此时需要为一个比较大的对象分配内存空间,而剩余的内存不足了,此时就会出现“Concurrent Mode Failure”的日志提示,表示并发回收垃圾失败,那么 JVM 此时就不得不启动备选方案,冻结用户线程,采用 Serial Old 垃圾回收器进行老年代的垃圾回收,在一定程度上,可能降低系统的性能。

第三,CMS 「会产生垃圾碎片」。CMS 使用的是标记-清除算法,所以会产生内存碎片,内存碎片可能会影响程序性能。如果要为大对象分配内存时,发现内存中没有一块能容得下该对象的内存,那这个时候 JVM 就不得不触发一次 Full GC,导致程序发生 STW。为了减少内存碎片,CMS 的研发人员提供了一个开关,当开启这个开关时,会让垃圾回收器在 Full GC 完成后,进行一次内存碎片的整理,这个参数就是:「-XX:+UseCMSCompactAtFullCollection」, 「+」 号表示开启开关。

这个开关虽然解决了内存碎片带来的问题,但是,如果每次在 Full GC 之后都进行一次内存碎片的整理,而每次内存碎片的整理又需要暂停用户线程,造成 STW,这会使 CMS 具有低延时的特点大打折扣,因此 CMS 的研发人员还提供了另外一个参数:「-XX:CMSFullGCsBeforeCompaction」,该参数表示的含义是当发生多少次 Full GC 后,才对内存进行一次整理。

8.2 CMS 发展现状及未来

CMS 是一款针对老年代的垃圾回收器,它需要与新生代的垃圾回收器搭配使用,而且只能与 Serial、ParNew 这两款新生代的垃圾回收器搭配。在 G1 垃圾回收器出现之前,CMS 曾经因为是唯一一款并发的、低延时的垃圾回收器,其应用十分广泛,但是后来被同样具有并发清理、低延时,但性能却比 CMS 高效很多倍的 G1 垃圾回收器打破了局面,以至于在 JDK9 中,CMS 被标记为「Deprecated」,开始逐渐的淡出人们的视野,在目前最新的 JDK14 中,CMS 则是完全被移除了,成为了第一款被彻底遗弃的垃圾回收器。

9.总结

本文详细介绍了 Serial、ParNew、Parallel Scavenge、SerialOld、CMS、Parallel Old 这六款垃圾回收器,最后用一张图来总结它们相互之间搭配使用的关系。需要说明的是,在 JDK9 中,取消了 ParNew 与 Serial Old、Serial 与 CMS 的搭配组合,并且 CMS 被标记 Deprecated,在 JDK14 中被彻底移除。

垃圾回收器搭配组合

10.参考

  • 周志明《深入理解 Java 虚拟机》第三版