1. 前言

Concurrent Mark Sweep (CMS) 收集器是hotspot虚拟机中一款低延迟并发型垃圾收集器。CMS垃圾收集器的关注点是:尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(低延迟)就是越适合与用户交互的程序,良好的响应速度能提升用户体验。

CMS 垃圾收集器 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除,详情可见 JEP 363。

不幸的是,CMS作为老年代的垃圾收集器,却无法与Jdk 1.4中以及存在的新生代收集器 Parallel Scavenge配合工作,所以在jdk 1.5 中 使用CMS来收集老年代的时候,新生代就只能选择 ParNew 或者Serial 收集器中的一个。

image

在G1 出现之前,CMS使用还是非常广泛的。

CMS 收集器通过命令行选项启用-XX:+UseConcMarkSweepGC。

2. CMS工作原理

CMS提出时最大的创新在于其针对老年代并发收集的理念,下面来分析并发收集的过程。根据oracle提供的关于CMS的官方文档描述

整个并发收集通常包括以下步骤:

image
  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除
  5. 并发重置

上述步骤只有初始化标记和重新标记会STW(Stop The World),其余三个步骤与应用程序mutator都是并发的,下面来看每个步骤具体的细节。

(1) 初始标记(Initial Mark)

初始标记 目标是标记老年代中的所有对象,这些对象包括直接的 GC Root关联的对象从年轻代中的某个活动对象引用的对象,该过程会发生第一次STW.

image

(2)并发标记

通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象 在并发标记阶段, 应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化。

比如:

  • 新生代的对象晋升到老年代
  • 直接在老年代分配对象
  • 老年代对象的引用关系发生变更

对于这些对象,需要重新标记以防止被遗漏。 为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card 标识为Dirty ,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

image

并发预清理 该阶段预标记线程和应用线程并行运行,此阶段将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,对于一些对象引用有变动的堆区域标记为Dirty 。

image

预清理阶段,这些脏对象被计算在内,并且可以从它们可到达的对象也被标记

image

可终止预清理

可<typo id="typo-1196" data-origin="中止" ignoretag="true">中止</typo>的并发预清理 这个阶段是尽量为下一个步骤减轻任务,默认时间是5s期望在时间内做一次YGC,从而减少下一次STW的时候扫描标记新生代引用老年代的对象个数。 由于这个阶段是循环<typo id="typo-1284" data-origin="的" ignoretag="true">的</typo>做两件事直到发生abort的条件,如:重复的次数、多少量的工作、持续的时间等:

  • 处理 From 和 To 区的对象,标记可达的老年代对象;
  • 和上一个阶段一样,扫描处理Dirty Card中的对象。

(3)最终标记

该阶段发生第二次STW, 目标是完成标记老年代中所有活动的对象,由于之前的预清理阶段是并发的,它们可能无法跟上应用程序的变化速度,所以需要STW 二次验证。

  • 遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
  • 根据GC Roots,重新标记;
  • 遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

在三个标记阶段之后,老年代的所有存活对象都被标记,现在垃圾收集器将通过清除老年代来回收所有未使用的对象:

(4)并发清除 与应用程序同时执行,无需STW。该阶段的目的是移除未使用的对象并回收它们占用的空间以备将来使用。

image

(5)并发重置 将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。 CSM执行日志:

2020-10-21T23:42:56.214-0800: [CMS-concurrent-mark-start]
2020-10-21T23:42:56.245-0800: [CMS-concurrent-mark: 0.031/0.031 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2020-10-21T23:42:56.245-0800: [CMS-concurrent-preclean-start]
2020-10-21T23:42:56.246-0800: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-10-21T23:42:56.246-0800: [CMS-concurrent-abortable-preclean-start]
 CMS: abort preclean due to time 2020-10-21T23:43:01.313-0800: [CMS-concurrent-abortable-preclean: 0.110/5.066 secs] [Times: user=0.15 sys=0.01, real=5.07 secs]
2020-10-21T23:43:01.313-0800: [GC (CMS Final Remark) [YG occupancy: 20889 K (59008 K)]2020-10-21T23:43:01.313-0800: [Rescan (parallel) , 0.0032949 secs]2020-10-21T23:43:01.316-0800: [weak refs processing, 0.0000792 secs]2020-10-21T23:43:01.317-0800: [class unloading, 0.0305113 secs]2020-10-21T23:43:01.347-0800: [scrub symbol table, 0.0108352 secs]2020-10-21T23:43:01.358-0800: [scrub string table, 0.0009403 secs][1 CMS-remark: 13558K(65536K)] 34447K(124544K), 0.0464377 secs] [Times: user=0.03 sys=0.02, real=0.04 secs]
2020-10-21T23:43:01.360-0800: [CMS-concurrent-sweep-start]
2020-10-21T23:43:01.367-0800: [CMS-concurrent-sweep: 0.007/0.007 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-10-21T23:43:01.367-0800: [CMS-concurrent-reset-start]
2020-10-21T23:43:01.367-0800: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
复制代码

3. CMS 优劣势

3.1 优点

  • 并发收集
  • 低停顿

所以CMS收集器适合与用户交互较多的场景,注重服务的响应速度,能给用户带来较好的体验!所以我们在做WEB开发的时候,经常会使用CMS收集器作为老年代的收集器!

3.2 缺点

  1. 会产生内存碎片, 导致并发清除后,用户线程可用的空间不足。在年轻代无法分配大对象的情况下,不得不提前触发Full GC。
  2. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程而导致引用程序变慢,总吞吐量会变低。
  3. CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure” 失败而导致另一次FUll GC 产生。在并发标记和并发清除阶段,用户线程与GC线程并发工作,这会导致在清理的时候又会有用户的线程在拼命的创建对象,本身垃圾回收时候肯定是可用内存不够了,可万一这时候用户线程创建了大量的对象怎么办呢?所以一般CMS收集器的垃圾回收的动作不会在完全无法分配内存的时候进行,可以通过“-XX:CMSInitiatingOccupancyFraction”参数来设置CMS预留的内存空间! 如果预留的空间无法满足CMS的需要,就会出现 “Concurrent Mode Failure”失败。 这时候JVM会启用后备方案,也就是前面介绍过的Serial Old收集器,这样会导致另一次的Full GC的产生,这样的代价是很大的,所以CMSInitiatingOccupancyFraction这个参数设置需要根据程序合理设置!

4. GC 分类

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,在整个过程中,经常对 Minor、Major、和 Full GC 事件的使用感到困惑。下面分别说明一下。

4.1 Minor GC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC

这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:

  1. JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
  2. 内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
  3. 执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
  4. 质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。

YGC是什么时候触发的?

大多数情况下,对象直接在年轻代中的Eden区进行分配,如果Eden区域没有足够的空间,那么就会触发YGC(Minor GC),YGC处理的区域只有新生代。因为大部分对象在短时间内都是可收回掉的,因此YGC后只有极少数的对象能存活下来,而被移动到S0区(采用的是复制算法)。

当触发下一次YGC时,会将Eden区和S0区的存活对象移动到S1区,同时清空Eden区和S0区。当再次触发YGC时,这时候处理的区域就变成了Eden区和S1区(即S0和S1进行角色交换)。每经过一次YGC,存活对象的年龄就会加1。

4.2 Major GC vs Full GC

大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的,Minor GC 清理年轻<typo id="typo-5038" data-origin="带" ignoretag="true">带</typo>内存应该被设计得简单:

  • Major GC 是清理永久代。
  • Full GC 是清理整个堆空间—包括年轻代和永久代。

Full GC又是什么时候触发的

下面4种情况,对象会进入到老年代中:

  • YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
  • 经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
  • 动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
  • 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代。除此之外,还有以下4种情况也会触发FGC:

  1. 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发FGC。
  2. 空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。
  3. Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
  4. System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。

5. CMS GC 参数配置

CMS(Concurrent Mark Sweep,并发-标记-清除)是目前最常用的 JVM 垃圾回收器,这里不解释 CMS 的工作过程,只记录一些基础要点以帮助理解后面的内容:

  • CMS 是一种基于并发、使用标记清除算法的垃圾回收器。CMS 会尽可能让 GC 线程与用户线程并发执行,可以消除长时间的 GC 停顿(STW)。
  • CMS 不会对新生代做垃圾回收,默认只针对老年代进行垃圾回收。此外,CMS 还可以开启对永久代的垃圾回收(或元空间),避免由于 PermGen 空间耗尽带来 Full GC,JDK6以上受参数 -XX:+CMSClassUnloadingEnabled 控制,这个参数在 JDK8 之前默认关闭,JDK8 默认开启了。
  • CMS 要与一个新生代垃圾回收器搭配使用,所谓"分代收集"。能与 CMS 配合工作的新生代回收器有 Serial 收集器和 ParNew 收集器,我们一般使用支持多线程执行的 ParNew 收集器。
  • 使用 CMS GC 策略<typo id="typo-6264" data-origin="时" ignoretag="true">时</typo>,GC 类别可以分为:Young GC(又称 Minor GC)Old GC(又称 Major GC、CMS GC),以及Full GC。其中 Full GC 是对整个堆的垃圾回收,STW 时间较长,对业务影响较大,应该尽量避免 Full GC

5.1 JVM 参数配置

经过理解各个参数的含义及取值影响,总结了以下的 JVM 参数配置,可以几乎不用调整使用:

-Xmx32g -Xms32g -Xmn1g Xss256k
-XX:SurvivorRatio=2 
-XX:MaxPermSize=256m
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:ParallelGCThreads=10
-XX:ParallelCMSThreads=16
-XX:+CMSParallelRemarkEnabled
-XX:MaxTenuringThreshold=15
-XX:+UseCMSCompactAtFullCollection
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:+CMSClassUnloadingEnabled
-XX:-DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:/app/log/hbase/gc-hbase-REGIONSERVER-`hostname`-`date +%Y%m%d`.log
复制代码

如果是 64G 及以上的大堆,-Xmn 可以调整到2g,其他参数不变或微调。下面对一些重要的 JVM 参数介绍说明。

重点参数解析

以下参数解析都建立在使用 CMS GC 策略基础上,这里使用 CMS GC 表示老年代垃圾回收,Young GC 表示新生代垃圾回收。 ① -Xmx, -Xms, -Xmn

  • -Xmx、-Xms 分别表示 JVM 堆<typo id="typo-7202" data-origin="的" ignoretag="true">的</typo>最大值,初始化大小。 -Xmx 等价于-XX:MaxHeapSize -Xms 等价于-XX:InitialHeapSize。
  • -Xmn表示新生代大小,等价于-XX:MaxNewSize、-XX:NewSize,这个参数的设置对 GC 性能影响较大,设置小了会影响 CMS GC 性能,设置大了会影响 Young GC 性能,建议取值范围在1~3g,比如32g堆大小时可以设为1g,64g堆大小时可以设为2g,通常性能会比较高。

-Xss

  • 表示线程栈的大小,等价于-XX:ThreadStackSize,默认1M,一般使用不了这么多,建议值256k。

-XX:SurvivorRatio

  • 新生代中 Eden <typo id="typo-7520" data-origin="区与" ignoretag="true">区与</typo> Survivor 区的比值,默认8,这个参数设置过大会导致 CMS GC 耗时过长,建议调小,使得短寿对象在Young区可以被充分回收,减少晋升到Old区的对象数量,以此提升 CMS GC 性能。

-XX:+UseParNewGC, -XX:+UseConcMarkSweepGC

  • 分别表示使用并行收集器 ParNew 对新生代进行垃圾回收,使用并发标记清除收集器 CMS 对老年代进行垃圾回收。

-XX:ParallelGCThreads, -XX:ParallelCMSThreads

  • 分别表示 Young GC 与 CMS GC 工作时的并行线程数,建议根据处理器数量进行合理设置。

-XX:MaxTenuringThreshold

  • 对象从新生代晋升到老年代的年龄阈值(每次 Young GC 留下来的对象年龄加一),默认值15,表示对象要经过15次 GC 才能从新生代晋升到老年代。设置太小会严重影响 CMS GC 性能,建议默认值即可。

-XX:+UseCMSCompactAtFullCollection

  • 由于 CMS GC 会产生内存碎片,且只在 Full GC 时才会进行内存碎片压缩(因此 使用 CMS 垃圾回收器避免不了 Full GC)。这个参数表示开启 Full GC 时的压缩功能,减少内存碎片。

-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction

  • -XX:CMSInitiatingOccupancyFraction 表示触发 CMS GC 的老年代使用阈值,一般设置为 70~80(百分比),设置太小会增加 CMS GC 发现的频率,设置太大可能会导致并发模式失败或晋升失败。默认为 -1,表示 CMS GC 会<typo id="typo-8318" data-origin="由" ignoretag="true">由</typo> JVM 自动触发。
  • -XX:+UseCMSInitiatingOccupancyOnly 表示 CMS GC 只基于 CMSInitiatingOccupancyFraction 触发,如果未设置该参数则 JVM 只会根据 CMSInitiatingOccupancyFraction 触发第一次 CMS GC ,后续还是会自动触发。建议同时设置这两个参数。

-XX:+CMSClassUnloadingEnabled

  • 表示开启 CMS 对永久代的垃圾回收(或元空间),避免由于永久代空间耗尽带来 Full GC。

6. 写在最后

hotspot 有这么多垃圾收集器,那么Serial GC、Prallel GC、CMS GC 有什么不同呢?

  • 如果想要最小化的使用内存和并行开销 就用 Serial GC
  • 如果想要最大化的使用吞吐量 就用 Prallel GC
  • 如果想要最小化的使用 GC中断和停顿时间(低延迟) 就用 CMS GC

同时注意,在Jdk9中默认使用 G1 垃圾收集器,将CMS标记为废弃。在Jdk14中移除了CMS垃圾收集器。