Serial GC 与 Parallel GC

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

分类

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

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

组合

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

VM参数组合结果
-XX:+UseSerialGCyoung Copy and old MarkSweepCompact
-XX:+UseG1GCyoung G1 Young and old G1 Mixed
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicyyoung PS Scavenge old PS MarkSweep (自适应调整)
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicyyoung PS Scavenge old PS MarkSweep, (非自适应调整)
-XX:+UseParNewGC (Java8中禁用,Java9已移除)young ParNew old MarkSweepCompact
-XX:+UseParNewGC -XX:+UseConcMarkSweepGCyoung 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
2
3
4
5
-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
2
3
4
5
-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
2
3
4
5
-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 收集器的使用。

参考资料

请我喝杯咖啡吧~