前面 我们学习了 JVM 中几种常用的 GC 算法,今天我们来学习一下 JVM 中实现这些算法的垃圾收集器。大多数 JVM 都需要使用两种不同的 GC 算法 —— 一种用来清理年轻代,另一种用来清理老年代。

分类

我们可以从以下几个不同的角度来对 GC 收集器进行分类:

  1. 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
    • 串行垃圾回收器一次只使用一个线程进行垃圾回收;
    • 并行垃圾回收器一次将开启多个线程同时进行垃圾回收。在并行能力较强的 CPU 上,使用并行垃圾回收器可以缩短 GC 的停顿时间。
  2. 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
    • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间;
    • 独占式垃圾回收器 (Stop the world) 一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。
  3. 按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。
    • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片;
    • 非压缩式的垃圾回收器不进行这步操作。
  4. 按工作的内存区间,又可分为新生代垃圾回收器和老年代垃圾回收器。

组合

由于新生代与老年代各自可以使用不同的垃圾收集器,因此两两在搭配使用时,会有许多种不同的组合。

VM 参数 组合结果
-XX:+UseSerialGC young Copy and old MarkSweepCompact
-XX:+UseG1GC young G1 Young and old G1 Mixed
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy young PS Scavenge old PS MarkSweep (自适应调整)
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy young PS Scavenge old PS MarkSweep, (非自适应调整)
-XX:+UseParNewGC (Java8 中禁用,Java9 已移除) young ParNew old MarkSweepCompact
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC young ParNew old ConcurrentMarkSweep
-XX:-UseParNewGC -XX:+UseConcMarkSweepGC (Java8 中禁用,Java9 已移除) young Copy old ConcurrentMarkSweep

其实,我们只需要重点关注以下 4 种组合,其他的在 Java8 之后的版本中已渐渐被抛弃……

  • 年轻代和老年代的串行 GC (Serial GC)
  • 年轻代和老年代的并行 GC (Parallel GC)
  • 年轻代的并行 GC (Parallel New) + 老年代的 CMS (Concurrent Mark and Sweep)
  • G1 负责回收年轻代和老年代

Serial / Serial Old(串行 GC)

Serial GC 对年轻代使用 mark­-copy (标记 ­ 复制) 算法,对老年代使用 mark­-sweep­-compact (标记 ­ 清除 ­ 整理) 算法。

Serial GC

特点

两者都是单线程的垃圾收集器,不能进行并行处理。两者都会触发 ** 全线暂停 (STW)**,停止所有的应用线程。

因此这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。

适用场合

该选项只适合几百 MB 堆内存的 JVM,而且是单核 CPU 时比较有用。 对于服务器端来说,因为一般是多个 CPU 内核,并不推荐使用,除非确实需要限制 JVM 所使用的资源。大多数服务器端应用部署在多核平台上,选择 Serial GC 就表示人为的限制系统资源的使用。 导致的就是资源闲置,多的 CPU 资源也不能用来降低延迟,也不能用来增加吞吐量。

Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

控制参数

  • -XX:+UseSerialGC:在新生代和老年代使用串行回收器。
  • -XX:+SurvivorRatio:设置 eden 区大小和 survivor 区大小的比例。
  • -XX:+PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
  • -XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC 后,对象年龄就加 1。任何大于这个年龄的对象,一定会进入老年代。

GC 日志分析

VM 参数
1
-client -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps ‐XX:+UseSerialGC
Minor GC
1
2016-06-15T16:05:03.173-0800: 196.835: [GC2016-06-15T16:05:03.173-0800: 196.836: [DefNew: 59007K->435K(59008K), 0.0103200 secs] 114651K->61875K(124544K), 0.0118620 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

2016-06-15T16:05:03.173-0800:GC 事件开始的时间。其中 ‐0800 表示东 8 时区。

196.835:GC 事件开始时,相对于 JVM 启动时的间隔时间,单位是秒。

GC :用来区分 Minor GC 还是 Full GC 的标志。 GC 表明这是一次 GC (Minor GC)

DefNew:垃圾收集器的名称。表示的是在年轻代中使用的:单线程,标记 ­ 复制 (mark­-copy),全线暂停 (STW) 垃圾收集器。

59007K->435K:在垃圾收集之前和之后年轻代的使用量。

(59008K):年轻代总空间大小

114651K->61875K:在垃圾收集之前和之后整个堆内存的使用情况。

(124544K):可用堆的总空间大小。

0.0118620 secs:GC 事件持续的时间,以秒为单位。

[Times: user=0.01 sys=0.00, real=0.01 secs]:GC 事件的持续时间,通过三个部分来衡量:

  • user – 在此次垃圾回收过程中,所有 GC 线程所消耗的 CPU 时间之和。
  • sys – GC 过程中操作系统调用和系统等待事件所消耗的时间。
  • real – 应用程序暂停的时间。因为串行垃圾收集器 (Serial Garbage Collector) 只使用单线程,因此 real time 等 于 user 和 system 时间的总和。

从上面的日志我们可以了解到:

  • GC 之前,堆内存使用总量为 114651K,年轻代使用总量为 59007K,那么老年代使用量为:55644K
  • GC 之后,堆内存减少了 52776K,年轻代减少了 58572K,那么表示有 5796K 的对象提升到了老年代。
Full GC
1
2016-06-15T16:10:31.467-0800: 525.130: [Full GC2016-06-15T16:10:31.467-0800: 525.130: [Tenured: 65536K->65535K(65536K), 0.2080570 secs] 124544K->111862K(124544K), [Perm : 31291K->31291K(131072K)], 0.2081880 secs] [Times: user=0.20 sys=0.00, real=0.21 secs]

Full GC:表示这一次是对整个堆空间进行清理。

[Tenured: 65536K->65535K(65536K), 0.2080570 secs]:老年代 GC 前为使用量为 65536K,GC 后使用量为 65535K,总空间为 65536K,使用 0.2080570 秒;

124544K->111862K(124544K):GC 前后整个堆内存使用量从 124544K 减少为 111862K,总空间为 124544K

[Perm : 31291K->31291K(131072K)], 0.2081880 secs]:持久区 GC 前后使用内存空间都为 31291K,没有变化。

Parallel Scavenge / Parallel Old GC(并行 GC)

Parallel GC 对年轻代使用 mark­-copy (标记 ­ 复制) 算法,对老年代使用 mark­-sweep­-compact (标记 ­ 清除 ­ 整理) 算法。

Parallel GC

特点

两者都是多线程的垃圾收集器,两者都会触发 ** 全线暂停 (STW)**,停止所有的应用线程。由于是多线程,所以相比于 Serial GC,垃圾收集时间将大大减少。

Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。

所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

适用场合

并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量:

  • 在 GC 期间,所有 CPU 内核都在并行清理垃圾,所以暂停时间更短
  • 在两次 GC 周期的间隔期,没有 GC 线程在运行,不会消耗任何系统资源

另一方面,因为此 GC 的所有阶段都不能中断,所以并行 GC 很容易出现长时间的卡顿。如果延迟是系统的主要目标,那么就应该选择其他垃圾收集器组合。

长时间卡顿的意思是,此 GC 启动之后,属于一次性完成所有操作,于是单次 pause 的时间会较长。

控制参数

  • ‐XX:ParallelGCThreads:指定 GC 线程数,默认值为 CPU 内核数。

  • -XX:MaxGCPauseMillis:最大垃圾收集停顿时间。不能设置过小,否则会降低系统吞吐量。

  • -XX:GCTimeRatio:设置吞吐量大小。是一个大于 0 且小于 100 的整数。假如设置为 99,则垃圾收集时间的比率为:1 / (1 + 99)。

  • -XX:+UseAdaptiveSizePolicy:这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小 (-Xmn)、Eden 与 Survivor 区的比例 (-XX:SurvivorRati)、晋升老年代对象年龄 (-XX:PretenureSizeThreshold) 等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC 自适应的调节策略( GC Ergonomics )。

GC 日志分析

VM 参数
1
-server -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseParallelGC -XX:+UseParallelOldGC

其中 -XX:+UseParallelGC -XX:+UseParallelOldGC 与使用 -XX:+UseParallelGC-XX:+UseParallelOldGC 等价。

Minor GC
1
2016-06-15T21:41:01.947-0800: 11.726: [GC [PSYoungGen: 51636K->8281K(54784K)] 112923K->72238K(120320K), 0.0062430 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

GC:小型 GC。

PSYoungGen:收集器名称,表示年轻代使用的是 Parallel Scavenge 收集器。

51636K->8281K(54784K):年轻代从 51636K 减少到了 8281K,年轻代空间总量为:54784K。

112923K-> 72238K(120320K):整个堆空间从 112923K 减少到了 72238K,堆空间总量为:120320K。

年轻代减少了 43355K,堆空间减少了 40685K,表明有 2670K 的对象从年轻代提升到了老年代。

Full GC
1
2016-06-15T21:41:01.953-0800: 11.732: [Full GC [PSYoungGen: 8281K->0K(54784K)] [ParOldGen: 63956K->59231K(65536K)] 72238K->59231K(120320K) [PSPermGen: 14462K->14462K(131072K)], 0.1389900 secs] [Times: user=0.44 sys=0.01, real=0.14 secs]

Full GC,表明这次清理了年轻代和老年代。

PSYoungGen: 8281K->0K(54784K):年轻代垃圾收集器为 Parallel Scavenge,使用量从 8281K 变为了 0K。年轻代空间总量为 54784K。

ParOldGen: 63956K->59231K(65536K):老年代收集器为 Parallel Old,使用量从 63956K 变为了 59231K。老年代空间总量为:65536K。

72238K->59231K(120320K):整个堆空间从 72238K 变为了 59231K。堆空间总量为:120320K。

PSPermGen: 14462K->14462K(131072K):持久去前后都为 14462K,没有变化。持久去空间总量为:131072K。

ParNew 收集器

ParNew 收集器本质上就是 Serial 收集器的多线程版本,其余行为包括控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样。

ParNew

控制参数

  • ‐XX:ParallelGCThreads:指定 GC 线程数,默认值为 CPU 内核数。

GC 日志分析

VM 参数
1
-server -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseParNewGC
Minor GC
1
2016-06-16T10:52:24.846-0800: 1.690: [GC2018-08-21T10:52:24.846-0800: 1.690: [ParNew: 52480K->6527K(59008K), 0.0143120 secs] 52480K->6683K(124544K), 0.0144440 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

ParNew:表明年轻代使用的是 ParNew 收集器

其他数据分析,与前面的日志分析一样

Full GC
1
2016-06-16T10:52:34.709-0800: 11.552: [Full GC2018-08-21T10:52:34.709-0800: 11.552: [Tenured: 65533K->65535K(65536K), 0.1024680 secs] 124534K->70147K(124544K), [Perm : 14425K->14425K(21248K)], 0.1025780 secs] [Times: user=0.10 sys=0.00, real=0.11 secs]

数据分析与前面的一样

注意

该种 -XX:+UseParNewGC 配置,在 Java8 中已不推荐使用,Java9 中已经完全删除了。在此后的版本中,只能与 CMS 收集器搭配使用。

总结

本篇文章我们讲了 Serial、Parallel、ParNew 的使用与配置,下篇文章我们将重点介绍 CMS 与 G1 收集器的使用。

参考资料