Concurrent Mark Sweep(CMS)

前面 我们介绍了 Serial GC 与 Parallel GC 的使用与配置,今天我们来介绍CMS GC的使用与配置。

概述

年轻代采用并行STW方式的 mark-copy(标记-复制) 算法,对老年代主要使用 mark-sweep(标记-清理) 算法。

CMS收集器的主要目的:减少老年代垃圾收集的停顿时间。主要通过两种手段来达成此目的:

  • 不对老年代进行整理,而是使用空闲列表(free-list)来管理内存空间的回收。
  • 在mark-sweep阶段采用并发方式与用户线程一起执行。

运作过程

CMS

CMS的主要分为5个阶段:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)、重置。

缺点

  • 对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4。CMS在并发阶段会占用部分CPU,导致用户应用程序执行速度变慢,影响用户体验。
  • 无法处理浮动垃圾(Floating Garbage)。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction 设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

  • 会产生大量的内存碎片空间。可以通过 -XX:UseCMSCompactAtFullCollection-XX:CMSFullGCsBeforeCompaction 这个两个参数来解决问题。

控制参数

  • -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用 CMS+串行收集器。

  • -XX:+ParallelCMSThreads:设定 CMS 的线程数量。

  • -XX:+CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%。

  • -XX:+UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。

  • -XX:+CMSParallelRemarkEndable: 启用并行重标记。

  • -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收。

  • -XX:CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 。

    前提条件: -XX:+CMSClassUnloadingEnabled

  • -XX:UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。

  • -XX:+CMSIncrementalMode:使用增量模式,比较适合单 CPU。

  • -XX:+UseCMSCompactAtFullCollection:(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。

GC日志分析

VM 参数
1
2
3
4
5
-server
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+UseConcMarkSweepGC
Minor GC
1
2016-06-18T11:20:33.960-0800: 4.118: [GC2016-06-18T11:20:33.961-0800: 4.119: [ParNew: 52480K->6528K(59008K), 0.0115760 secs] 52480K->6679K(255616K), 0.0120110 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]

日志分析和 前面 差不多,这里不细说

Full GC

此前我们介绍的GC日志格式都差不多,而CMS Full GC的格式则完全不同,如下是CMS对老年代进行垃圾收集各阶段的日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
2016-06-18T11:20:41.197-0800: 11.355: [GC [1 CMS-initial-mark: 98970K(196608K)] 106560K(255616K), 0.0020160 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-06-18T11:20:41.199-0800: 11.357: [CMS-concurrent-mark-start]
2016-06-18T11:20:41.263-0800: 11.421: [CMS-concurrent-mark: 0.063/0.063 secs] [Times: user=0.10 sys=0.01, real=0.07 secs]
2016-06-18T11:20:41.263-0800: 11.421: [CMS-concurrent-preclean-start]
2016-06-18T11:20:41.264-0800: 11.422: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2016-06-18T11:20:41.264-0800: 11.422: [CMS-concurrent-abortable-preclean-start]
2016-06-18T11:20:41.365-0800: 11.523: [GC2016-06-18T11:20:41.365-0800: 11.523: [ParNew2016-06-18T11:20:41.372-0800: 11.530: [CMS-concurrent-abortable-preclean: 0.007/0.108 secs] [Times: user=0.08 sys=0.02, real=0.11 secs]
: 59008K->6528K(59008K), 0.0084290 secs] 157978K->108370K(255616K), 0.0085350 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2016-06-18T11:20:41.374-0800: 11.532: [GC[YG occupancy: 7578 K (59008 K)]2016-06-18T11:20:41.374-0800: 11.532: [Rescan (parallel) , 0.0027870 secs]2016-06-18T11:20:41.377-0800: 11.535: [weak refs processing, 0.0027290 secs]2016-06-18T11:20:41.380-0800: 11.538: [scrub string table, 0.0007810 secs] [1 CMS-remark: 101842K(196608K)] 109420K(255616K), 0.0066720 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2016-06-18T11:20:41.382-0800: 11.540: [CMS-concurrent-sweep-start]
2016-06-18T11:20:41.412-0800: 11.570: [CMS-concurrent-sweep: 0.031/0.031 secs] [Times: user=0.05 sys=0.01, real=0.03 secs]
2016-06-18T11:20:41.413-0800: 11.571: [CMS-concurrent-reset-start]
2016-06-18T11:20:41.413-0800: 11.571: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

阶段 1: Initial Mark(初始标记 )。这是第一次STW事件。此阶段的目标是标记老年代中所有存活的对象,包括 GC Roots 的直接引用,以及由年轻代中存活对象所引用的对象。

04_06_g1-06

1
2016-06-18T11:20:41.197-0800: 11.355: [GC [1 CMS-initial-mark: 98970K(196608K)] 106560K(255616K), 0.0020160 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

CMS-initial-mark:表示CMS垃圾收集第一个阶段——初始标记。

98970K(196608K):表示老年代当前的使用总量为 98970K,老年代总的内存数量为:196608K

106560K(255616K):表示当前堆的使用总量为 106560K,堆内存总量为:255616K

阶段 2: Concurrent Mark(并发标记 )。在此阶段,垃圾收集器遍历老年代,标记所有的存活对象,从前一阶段 “Initial Mark” 找到的 root 根开始算起。 顾名思义,“并发标记”阶段,就是与应用程序同时运行,不用暂停的阶段。 请注意,并非所有老年代中存活的对象都在此阶段被标记,因为在标记过程中对象的引用关系还在发生变化。

04_07_g1-07

1
2
2016-06-18T11:20:41.199-0800: 11.357: [CMS-concurrent-mark-start]
2016-06-18T11:20:41.263-0800: 11.421: [CMS-concurrent-mark: 0.063/0.063 secs] [Times: user=0.10 sys=0.01, real=0.07 secs]

CMS-concurrent-mark-start:并发标记开始

CMS-concurrent-mark:CMS垃圾回收的并发标记阶段。

0.063/0.063 secs:此阶段的持续时间,分别是运行时间和相应的实际时间。

阶段 3: Concurrent Preclean(并发预清理)。此阶段同样是与应用线程并行执行的,不需要停止应用线程。 因为前一阶段是与程序并发进行的,可能有一些引用已经改变。如果在并发标记过程中发生了引用关系变化,JVM会(通过“Card”)将发生了改变的区域标记为“脏”区(这就是所谓的 卡片标记Card Marking )。

04_08_g1-08

在预清理阶段,这些脏对象会被统计出来,从他们可达的对象也被标记下来。此阶段完成后, 用以标记的 card 也就被清空了。

04_09_g1-09

此外,本阶段也会执行一些必要的细节处理,并为 Final Remark 阶段做一些准备工作。

1
2
2016-06-18T11:20:41.263-0800: 11.421: [CMS-concurrent-preclean-start]
2016-06-18T11:20:41.264-0800: 11.422: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

CMS-concurrent-preclean:并发预清理阶段,统计此前的标记阶段中发生了改变的对象。

0.001/0.001 secs:此阶段的持续时间,分别是运行时间和对应的实际时间。

阶段 4: Concurrent Abortable Preclean(并发可取消的预清理)。 此阶段也不停止应用线程。本阶段尝试在 STW的 Final Remark 之前尽可能地多做一些工作。本阶段的具体时间取决于多种因素,因为它循环做同样的事情,直到满足某个退出条件( 如迭代次数、有用工作量、消耗的系统时间等等)。

1
2
3
2016-06-18T11:20:41.264-0800: 11.422: [CMS-concurrent-abortable-preclean-start]
2016-06-18T11:20:41.365-0800: 11.523: [GC2016-06-18T11:20:41.365-0800: 11.523: [ParNew2016-06-18T11:20:41.372-0800: 11.530: [CMS-concurrent-abortable-preclean: 0.007/0.108 secs] [Times: user=0.08 sys=0.02, real=0.11 secs]
: 59008K->6528K(59008K), 0.0084290 secs] 157978K->108370K(255616K), 0.0085350 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

CMS-concurrent-abortable-preclean:并发可取消的预清理。

阶段 5: Final Remark(最终标记)。这是此次GC事件中第二次(也是最后一次)STW阶段。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的 preclean 阶段是并发的,有可能无法跟上应用程序的变化速度。所以需要 STW暂停来处理复杂情况。

通常CMS会尝试在年轻代尽可能空的情况运行 final remark 阶段,以免接连多次发生 STW 事件。

Java7的日志如下

1
2016-06-18T11:20:41.374-0800: 11.532: [GC[YG occupancy: 7578 K (59008 K)]2016-06-18T11:20:41.374-0800: 11.532: [Rescan (parallel) , 0.0027870 secs]2016-06-18T11:20:41.377-0800: 11.535: [weak refs processing, 0.0027290 secs]2016-06-18T11:20:41.380-0800: 11.538: [scrub string table, 0.0007810 secs] [1 CMS-remark: 101842K(196608K)] 109420K(255616K), 0.0066720 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

Java8的日志如下

1
2016-06-18T16:10:41.353-0800: 11.046: [GC (CMS Final Remark) [YG occupancy: 7576 K (59008 K)]11.046: [Rescan (parallel) , 0.0048835 secs]11.051: [weak refs processing, 0.0007074 secs]11.052: [class unloading, 0.0145890 secs]11.067: [scrub symbol table, 0.0027903 secs]11.069: [scrub string table, 0.0023910 secs][1 CMS-remark: 104562K(196608K)] 112139K(255616K), 0.0303861 secs] [Times: user=0.05 sys=0.01, real=0.03 secs]

[YG occupancy: 7578 K (59008 K)]:年轻代使用量7578K,总量为 59008 K。

[Rescan (parallel) , 0.0027870 secs]:应用程序暂定,重新扫描,完成所有存活对象的标记。耗时:0.0027870秒

[weak refs processing, 0.0027290 secs]:第一个子阶段,处理弱引用的

[class unloading, 0.0145890 secs]:第二个子阶段,卸载不使用的类。(Java8多出了的一步)

[scrub string table, 0.0007810 secs]:第三个子阶段,清洗内部化字符串对应的 string tables

101842K(196608K):此阶段完成后老年代的使用量和总量

109420K(255616K):此阶段完成后整个堆内存的使用量和总量

阶段 6: Concurrent Sweep(并发清除 ) 。此阶段与应用程序并发执行, 不需要STW停顿。目的是删除未使用的对象,并收回他们占用的空间。

04_10_g1-10

1
2
2016-06-18T11:20:41.382-0800: 11.540: [CMS-concurrent-sweep-start]
2016-06-18T11:20:41.412-0800: 11.570: [CMS-concurrent-sweep: 0.031/0.031 secs] [Times: user=0.05 sys=0.01, real=0.03 secs]

CMS-concurrent-sweep: 0.031/0.031 secs :并发清理阶段,清理掉不再使用的垃圾对象。

阶段 7: Concurrent Reset(并发重置) 。此阶段与应用程序并发执行,重置CMS算法相关的内部数据,为下一次GC循环做准备。

1
2
2016-06-18T11:20:41.413-0800: 11.571: [CMS-concurrent-reset-start]
2016-06-18T11:20:41.413-0800: 11.571: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

CMS-concurrent-reset: 0.000/0.000 secs:并发重置阶段,以及使用时间。

参考资料

请我喝杯咖啡吧~