CMS GC

1. 概述

CMS,Most concurrently,是一款 采用 并发-标记-清除(Concurrent-Mark-Sweep)算法、针对老年代的 GC,其目的是最小化暂停时间。

CMS GC 在 JDK 9 被标记为 deprecated。

2. 执行过程

CMS 的标记过程采用指针可达性分析方法,对于存活的对象,在对象头上进行标记。

2.1 初始标记

从 GC Roots、新生代(Eden、From、To)出发标记直接关联的对象。

需要 STW 。

2.2 并发标记

该阶段进行 GC Root Tracing,从初始标记阶段 标记过的对象出发,标记所有可达的对象。

与应用线程并发执行,可以采用多个标记线程进行并发标记。

2.3 预清理

此阶段从新生代晋升的对象、新分配到老年代的对象以及在并发阶段修改了的对象 出发进行标记。
在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的 card 标记为 dirty,通过扫描这些 card,重新标记那些在并发阶段引用被更新的对象。

通过 -XX:-CMSPrecleaningEnabled 参数选择关闭该阶段,默认开启。

目的是减少重新标记阶段的暂停时间。

CMS 提供了参数 CMSScavengeBeforeRemark 在 重新标记之前强制进行一次 minor GC,这样做的好处是缩短了重新标记阶段的暂停时间,坏处是 Minor GC 后紧跟着一个 重新标记的 暂停,这样使停顿时间也比较久。

2.4 可中断预清理

该阶段存在的目的是减少重新标记的工作量,减少暂停时间。主要做两件事:

    1. 扫描 dirty card 中的对象;
    1. 处理新生代引用到老年代的对象。

在预清理步骤后,如果满足下面两个条件,就不会开启可中断的预清理,直接进入重新标记阶段:

  • Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这个参数的默认值是2M;
  • Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这个参数的默认值是50%。

该阶段退出的条件有三个:

  1. -XX:CMSAbortablePrecleanTime=n 参数控制,默认5秒退出;该阶段持续的时间超过 n 秒。
  2. Eden 区达到 -XX:CMScheduleRemarkEdenPenetration=n 参数配置的值(默认 50%);eden 的使用率超过某个阈值。
  3. -XX:CMSMaxAbortablePrecleanLoops=n 控制的扫描次数(默认是0表示限制次数)。循环次数限制。

该阶段是希望发生一次 Minor GC,这样就可以减少 Eden 区对象的数量,降低重新标记的工作量,因为重新标记需要扫描整个 Eden(不扫描 from、to 是因为新对象不会分配在这里,初始标记阶段扫描一次就够了)。

2.5 重新标记

主要遍历三个地方进行重新标记:

  1. Eden区(To survivor 应该也要?)
  2. Dirty card
  3. GC Roots

此阶段是 STW 的。

在重新标记阶段,如果新生代有很多存活的对象,就会导致 STW 很长,CMS 提供参数 -XX:+CMSScavengeBeforeRemark 来强制在进入此阶段前进行 minor gc 。

2.6 并发清理

并发扫描堆中的每一个对象,清理未标记的对象(垃圾),对于已标记的对象清理其标记位,以便下次回收扫描。

2.7 并发重置

重置 CMS内部的数据结构。

3. 异常情况

3.1 并发模式失败

新生代发生垃圾回收,同事老年代有没有足够的空间容纳晋升的对象时,就发生了 concurrent mode failure,CMS 垃圾回收就会退化成 Full GC,这个回收过程是单线程的。

发生并发模式失效往往是由于 CMS 不能以足够快的速度清理老年代空间:新生代需要进行垃圾回收时,CMS 收集器计算发现老年代没有足够的空闲空间可以容纳这些晋升对象,不得不先对老年代进行垃圾回收。

并发模式并不必须导致 Full GC,在特定的情况下,JVM 将只是等待并发收集周期结束,但应用程序将处于 STW 状态直至 YGC 结束。

并发模式失败通常是由于 CMS 周期开始得太晚。

3.2 晋升失败

新生代做 minor gc 时,需要 CMS 的担保机制确认老年代是否有足够的空间容纳晋升的对象;如果担保机制发现不够,则报 concurrent mode failure;如果担保机制判断是够的,但实际由于碎片问题导致无法分配,则报晋升失败。

出现晋升失败时,CMS 收集器在新生代垃圾收集过程中(STW),对整个老年代空间进行了整理和压缩。由于需要对整个堆进行整理,暂停时间比并发模式失败要长很多。

3.3 永久代耗尽

默认情况下,CMS 不会对永久代进行收集,一旦永久代空间耗尽,触发 Full GC。

4. CMS GC 的触发条件

  1. 周期性触发:后台线程 “ConcurrentMarkSweepThread” 循环(默认间隔2秒)判断是否需要触发,通过设置 -XX:-UseCMSInitiatingOccupancyOnly 来禁止这个自行判断。

  2. 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认 92%

5. Full GC

Full GC 是在 STW 下进行。

Full GC 并不总是进行压缩,通过参数 -XX:CMSFullGCsBeforeCompaction=<N> 设置经过 N 次不压缩的 Full GC 后执行一次带压缩的 Full GC。
-XX:+UseCMSCompactAtFullCollection,默认为 true,在进行 Full GC 时进行内存整理。

5.1 触发条件

  1. 显式调用 System.gc() 、且没有开启参数 -XX:+ExplicitGCInvokesConcurrent
  2. 通过工具 jmap -histo:live <pid> 显式触发;
  3. 晋升失败或并发模式失败;
  4. 统计得到的 Minor GC 晋升到老年代的平均大小大于老年代的剩余空间;
  5. 老年代或永久代 OOM 。
  6. 元空间达到 MaxMetaspaceSize 。

6. 为什么不推荐 CMS GC

来自 http://hellojava.info/?p=142

  1. 触发比例不好设置。
  2. 抢占 CPU。
  3. YGC 速度变慢。晋升时需要查找空闲空间。
  4. 碎片问题带来严重后果。

CMS GC 有70 多个参数可设置,调优复杂。

7. 相关参数

启用 CMS GC: -XX:+UseConcMarkSweepGC ,默认搭配的新生代 GC 是 ParNew,即启用 CMS 默认同时激活 -XX:+UseParNewGC

通过 -XX:+CMSParallelInitialMarkEnabled 参数可以开启该阶段的并行标记,使用多个线程进行标记,减少暂停时间。

关闭预清理阶段:通过 -XX:-CMSPrecleaningEnabled,默认开启。

7.3 可中断预清理相关

新生代 Eden 区的内存使用量大于参数 -XX:CMSScheduleRemarkEdenSizeThreshold =2m 执行可中断预清理。

-XX:CMSMaxAbortablePrecleanLoops=n :设置最多循环的次数,默认是 0 表示没有限制。

-XX:CMSAbortablePrecleanTime=5 限制执行可中断预清理的最长时间,单位是秒,默认是 5,达到这个时间退出循环。

-XX:CMSScheduleRemaikEdenPenetration=50 Eden 区的使用率达到这个百分比退出循环。

9. 调优

9.1 晋升失败

  • 令老年代回收尽早开始,增大回收频率。
  • 增大老年代空间。
  • 增大新生代空间,提高对象滞留时间,更多的对象被回收而不是晋升。
  • 增加更多的后台回收线程。

9.2 并发模式失败

  • 有难度,因为CMS本身不能 Compat 内存,只能退化到 SerialGC 来做。
  • 尝试用G1,G1的内存模型更加先进。JDK 11 引入的 ZGC 应该是更好的选择。

10. 参考资料

JVM 源码解读之 CMS GC 触发条件
CMS垃圾收集器

11. gc 日志

abortable-preclean 等来了一次 YGC :

2019-12-19T10:08:59.836+0800: 9.146: [GC (CMS Initial Mark) [1 CMS-initial-mark: 29316K(174784K)] 35958K(253440K), 0.0018362 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-12-19T10:08:59.838+0800: 9.148: [CMS-concurrent-mark-start]
2019-12-19T10:08:59.867+0800: 9.177: [CMS-concurrent-mark: 0.029/0.029 secs] [Times: user=0.13 sys=0.00, real=0.03 secs] 
2019-12-19T10:08:59.867+0800: 9.177: [CMS-concurrent-preclean-start]
2019-12-19T10:08:59.868+0800: 9.178: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-12-19T10:08:59.868+0800: 9.178: [CMS-concurrent-abortable-preclean-start]

2019-12-19T10:09:00.050+0800: 9.360: [GC (Allocation Failure) 2019-12-19T10:09:00.050+0800: 9.360: [ParNew: 75352K->5384K(78656K), 0.0046068 secs] 104668K->35432K(253440K), 0.0046823 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 

2019-12-19T10:09:00.195+0800: 9.506: [CMS-concurrent-abortable-preclean: 0.142/0.327 secs] [Times: user=1.15 sys=0.00, real=0.33 secs] 
2019-12-19T10:09:00.196+0800: 9.506: [GC (CMS Final Remark) [YG occupancy: 49922 K (78656 K)]2019-12-19T10:09:00.196+0800: 9.506: [Rescan (parallel) , 0.0108109 secs]2019-12-19T10:09:00.206+0800: 9.517: [weak refs processing, 0.0000196 secs]2019-12-19T10:09:00.206+0800: 9.517: [class unloading, 0.0058067 secs]2019-12-19T10:09:00.212+0800: 9.523: [scrub symbol table, 0.0079906 secs]2019-12-19T10:09:00.220+0800: 9.531: [scrub string table, 0.0008547 secs][1 CMS-remark: 30048K(174784K)] 79970K(253440K), 0.0260276 secs] [Times: user=0.05 sys=0.00, real=0.03 secs] 
2019-12-19T10:09:00.222+0800: 9.532: [CMS-concurrent-sweep-start]
2019-12-19T10:09:00.230+0800: 9.541: [CMS-concurrent-sweep: 0.009/0.009 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
2019-12-19T10:09:00.230+0800: 9.541: [CMS-concurrent-reset-start]
2019-12-19T10:09:00.231+0800: 9.541: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

[YG occupancy: 49922 K (78656 K)] 表示年轻代的使用情况。

[Rescan (parallel) , 0.0108109 secs] 表示重新标记阶段并发扫描的耗时。

[weak refs processing, 0.0000196 secs] 表示对弱引用的处理耗时。

[class unloading, 0.0058067 secs] 表示卸载无用的类的耗时。

[scrub symbol table, 0.0079906 secs] 表示清理分别包含类级元数据和内部化字符串的符号和字符串表的耗时。

[scrub string table, 0.0008547 secs] 清理

[1 CMS-remark: 30048K(174784K)] 79970K(253440K), 0.0260276 secs] 表示老年代、整个堆的内存使用情况。

通过 jmap -histo:live <pid> 触发的 full gc

2019-12-19T10:53:04.251+0800: 81.175: [Full GC (Heap Inspection Initiated GC) 2019-12-19T10:53:04.251+0800: 81.175: [CMS: 61284K->57074K(174784K), 0.1813809 secs] 85360K->57074K(253440K), [Metaspace: 67599K->67599K(1110016K)], 0.1814978 secs] [Times: user=0.19 sys=0.00, real=0.18 secs] 

元空间扩容导致的 gc

2019-12-19T11:10:26.975+0800: 23.032: [Full GC (Metadata GC Threshold) 2019-12-19T11:10:26.975+0800: 23.032: [CMS2019-12-19T11:10:27.041+0800: 23.099: [CMS-concurrent-mark: 0.072/0.072 secs] [Times: user=0.06 sys=0.00, real=0.07 secs] 
 (concurrent mode failure): 56826K->55560K(174784K), 0.2376781 secs] 58594K->55560K(253504K), [Metaspace: 64259K->64259K(1107968K)], 0.2378476 secs] [Times: user=0.23 sys=0.00, real=0.24 secs] 
2019-12-19T11:10:27.213+0800: 23.270: [Full GC (Last ditch collection) 2019-12-19T11:10:27.213+0800: 23.270: [CMS: 55560K->49199K(174784K), 0.1635283 secs] 55560K->49199K(253504K), [Metaspace: 64259K->64259K(1107968K)], 0.1638558 secs] [Times: user=0.17 sys=0.00, real=0.16 secs] 

Metadata GC Threshold:表示 metaspace 空间不能满足分配时触发,这个阶段不会清理软引用;
Last ditch collection:经过 Metadata GC Threshold 触发的 full gc 后还是不能满足条件,再触发一次 gc cause 为 Last ditch collection 的 full gc,这次 full gc 会清理软引用。

各种 GC 的触发原因解释见:Java GC Causes Distilled


-XX:+AlwaysTenure YGC 时 JVM 直接把对象晋升到老年代而不是拷贝到 survivor 空间。

晋升的两个条件:年龄达到 –XX:MaxTenuringThreshold 或目标 survivor 的使用率达到 –XX:TargetSurvivorRatio

-XX:PretenureSizeThreshold=<n> 指示 JVM 把大小大于 n 字节的对象直接分配到老年代。
如果对象的大小大于 eden 空间也会直接分配到老年代。
系统对象总是直接分配到永久代。


欢迎关注我的微信公众号: coderbee笔记

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据