Serial GC 与 Parallel GC
前面 我们学习了 JVM 中几种常用的 GC 算法,今天我们来学习一下 JVM 中实现这些算法的垃圾收集器。大多数 JVM 都需要使用两种不同的 GC 算法 —— 一种用来清理年轻代,另一种用来清理老年代。
分类
我们可以从以下几个不同的角度来对 GC 收集器进行分类:
- 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。
- 串行垃圾回收器一次只使用一个线程进行垃圾回收;
- 并行垃圾回收器一次将开启多个线程同时进行垃圾回收。在并行能力较强的 CPU 上,使用并行垃圾回收器可以缩短 GC 的停顿时间。
- 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间;
- 独占式垃圾回收器 (Stop the world) 一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。
- 按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片;
- 非压缩式的垃圾回收器不进行这步操作。
- 按工作的内存区间,又可分为新生代垃圾回收器和老年代垃圾回收器。
组合
由于新生代与老年代各自可以使用不同的垃圾收集器,因此两两在搭配使用时,会有许多种不同的组合。
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 (标记 清除 整理) 算法。
特点
两者都是单线程的垃圾收集器,不能进行并行处理。两者都会触发 ** 全线暂停 (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 (标记 清除 整理) 算法。
特点
两者都是多线程的垃圾收集器,两者都会触发 ** 全线暂停 (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 收集器完全一样。
控制参数
‐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 收集器的使用。
参考资料
- 《深入理解 Java 虚拟机(第 2 版)》第 3 章节「垃圾收集器与内存分配策略」
- https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/index.html
- http://www.fasterj.com/articles/oraclecollectors1.shtml
- https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html
- http://java-latte.blogspot.com/2013/08/garbage-collection-in-java.html
- http://www.cnblogs.com/iceAeterNa/p/4877549.html
- https://www.cnblogs.com/iceAeterNa/p/4877741.html
- https://github.com/cncounter/gc-handbook