Concurrent Mark Sweep (CMS) Collector
并发标记扫描(CMS)收集器
并发标记扫描(Concurrent Mark Sweep,CMS)收集器专为希望缩短垃圾收集暂停时间,并愿意在应用程序运行时与垃圾收集器共享处理器资源的应用场景而设计。通常,拥有相对较大的老年代(Tenured Generation)数据集,且在拥有两个或更多处理器的计算机上运行的应用程序,往往能从此收集器中受益。对于任何对暂停时间要求较低的应用程序,都应考虑使用该收集器。
CMS 收集器可通过命令行选项 -XX:+UseConcMarkSweepGC 启用。
与其他可用收集器类似,CMS 收集器也是分代收集器(Generational Collector)。因此,会发生年轻代收集(Minor Collection)和老年代收集(Major Collection)。CMS 收集器尝试通过使用单独的垃圾收集线程,在执行应用程序线程的同时跟踪可访问对象,从而减少因老年代收集导致的暂停时间。在每个主要的收集周期中,CMS 收集器会在收集开始时暂停所有应用程序线程一小段时间,并在收集中间再次暂停。第二次停顿通常是两次停顿中较长的一个。在这两次暂停期间,均使用多个线程来执行收集工作。
并发模式失败
CMS 收集器使用一个或多个垃圾收集线程与应用程序线程同时运行,目的是在老年代填满之前完成收集。如前所述,在正常操作中,CMS 收集器在应用程序线程仍在运行的情况下执行其大部分跟踪和清除工作,因此应用程序线程仅会看到短暂的暂停。
但是,如果 CMS 收集器无法在老年代填满之前完成对不可访问对象的回收,或者如果老年代的可用空闲空间块无法满足分配要求,应用程序将被暂停,且所有应用程序线程均停止。这种无法同时完成收集的过程称为并发模式失败(Concurrent Mode Failure),表示需要调整 CMS 收集器参数。如果并发收集被显式垃圾收集(System.gc())中断,或者需要为诊断工具提供信息的垃圾收集中断,则将报告并发模式中断。
过多的 GC 时间和 OutOfMemoryError
如果在垃圾收集上花费了总时间的 98% 以上,而回收不到 2% 的堆,CMS 收集器将抛出 OutOfMemoryError。此功能旨在防止应用程序因堆太小而长时间运行却几乎没有进展。如有必要,可以通过在命令行中添加选项 -XX:-UseGCOverheadLimit 来禁用此功能。
该策略与并行收集器中的策略相同,除了执行并发收集所花费的时间不计入 98% 的时间限制。换句话说,只有在应用程序停止时执行的收集才计入过多的 GC 时间。此类收集通常是由于并发模式失败或显式收集请求(例如对 System.gc() 的调用)引起的。
浮动垃圾
与 Java HotSpot VM 中的所有其他收集器一样,CMS 收集器是一个跟踪收集器,它至少标识堆中的所有可访问对象。在 Richard Jones 和 Rafael D. Lins 的出版物《_垃圾收集:自动动态内存算法_》中,它被归类为增量更新收集器。
由于应用程序线程和垃圾收集器线程在主要收集期间同时运行,垃圾收集器线程跟踪的对象随后可能会在收集过程结束时变得不可访问。尚未回收的此类不可访问的对象称为浮动垃圾(Floating Garbage)。_浮动垃圾_的量取决于并发收集周期的持续时间以及应用程序更新引用的频率(也称为_突变器 (Mutator)_)。
此外,由于年轻代和老年代是独立收集的,因此彼此互为根。作为粗略的指导,尝试将永久代(Permanent Generation)的大小增加 20%,以解决浮动垃圾的问题。在一个并发收集周期结束时,堆中的浮动垃圾将在下一个收集周期中被收集。
暂停
CMS 收集器在并发收集周期中两次暂停应用程序:
- 初始标记(Initial Mark):第一个暂停是将可从根直接访问的对象(例如,来自应用程序线程堆栈和寄存器的对象引用、静态对象等)以及从堆中其他位置(例如年轻代)直接可达的对象标记为活动状态。
- 重新标记(Remark):第二个暂停发生在并发跟踪阶段的末尾,用于查找由于 CMS 收集器完成对对象的引用后,应用程序线程对对象中的引用进行了更新而导致并发跟踪遗漏的对象。
并发阶段
可达对象图的并发跟踪发生在初始标记暂停和重新标记暂停之间。在此并发跟踪阶段,一个或多个并发垃圾收集器线程可能正在使用处理器资源,否则这些资源将可供应用程序使用。结果,即使没有暂停应用程序线程,在此阶段和其他并发阶段中,受计算绑定的应用程序的吞吐量也可能会相应下降。
重新标记暂停后,并发清除阶段将收集标识为不可访问的对象。收集周期完成后,CMS 收集器将等待,几乎不消耗任何计算资源,直到下一个主要收集周期开始。
开始并发收集周期
使用串行收集器时,只要老年代已满并且在完成收集时所有应用程序线程都将停止,就会发生主要收集。相反,并发收集的开始必须定时,以使收集可以在老年代变满之前完成。否则,由于并发模式失败,应用程序将观察到更长的暂停。有几种启动并发收集的方法。
根据最近的历史记录,CMS 收集器将保留对永久代用尽之前的剩余时间以及并发收集周期所需时间的估计。使用这些动态估计,开始并发收集周期,目的是在老年代用完之前完成收集周期。为了安全起见,对这些估计值进行了填充,因为并发模式失败的代价可能很高。
如果老年代的占用量超过初始占用量(占老年代的百分比),则并发收集也将开始。此初始占用阈值的默认值约为 92%,但是该值可能会因版本而异。可以使用命令行选项手动调整此值 -XX:CMSInitiatingOccupancyFraction=<N>,其中 <N> 是老年代大小的整数百分比(0 到 100)。
安排暂停
年轻代收集和老年代收集的暂停独立发生。它们不会重叠,但可能会快速连续发生,因此一个收集的暂停紧接着另一个收集的暂停,可能看起来像是一个较长的暂停。为了避免这种情况,CMS 收集器尝试在上次和下一个年轻代暂停之间的大致中间时间安排重新标记暂停。当前尚未为初始标记暂停执行此计划,该时间通常比重新标记暂停短得多。
增量模式
注意:增量模式在 Java SE 8 中已不推荐使用,并且在将来的主要版本中可能会删除它。
CMS 收集器可以在并发阶段以增量方式完成的模式下使用。回想一下,在并发阶段,垃圾收集器线程正在使用一个或多个处理器。增量模式旨在通过定期停止并发阶段以使处理器退还给应用程序,来减轻长时间并发阶段的影响。这种模式在这里称为 i-cms,它将收集器同时完成的工作划分为年轻代收集之间安排的一小段时间。当需要 CMS 收集器提供的低暂停时间的应用程序在具有少量处理器(例如 1 或 2)的计算机上运行时,此功能很有用。
并发收集周期通常包括以下步骤:
- 停止所有应用程序线程,从根目录确定可访问的对象集,然后继续所有应用程序线程。
- 在应用程序线程正在执行的同时,使用一个或多个处理器跟踪可访问对象图。
- 同时使用一个处理器回溯自上一步中的跟踪以来修改的对象图的各个部分。
- 停止所有应用程序线程,并回溯自上次检查以来可能已被修改的根和对象图中的部分,然后恢复所有应用程序线程。
- 同时使用一个处理器将无法访问的对象清除到用于分配的空闲列表中。
- 同时调整堆的大小,并使用一个处理器为下一个收集周期准备支持数据结构。
通常,CMS 收集器在整个并发跟踪阶段使用一个或多个处理器,而不会自愿放弃它们。同样,一个处理器用于整个并发扫描阶段,而不会放弃它。对于具有响应时间限制的应用程序(否则可能已经使用处理核心)的应用程序,这种开销可能会造成很大的破坏,尤其是在仅具有一个或两个处理器的系统上运行时。增量模式通过将并发阶段分解为短暂的活动突发来解决此问题,这些活动计划在较小的暂停之间进行。
i-cms 模式使用占空比(Duty Cycle)控制自愿放弃处理器之前允许 CMS 收集器执行的工作量。该占空比是年轻代收集之间的时间百分比,该 CMS 收集器允许运行。i-cms 模式可以根据应用程序的行为自动计算占空比(推荐的方法,称为_自动定步_),也可以在命令行上将占空比设置为固定值。
命令行选项
表 8-1"i-cms 的命令行选项” 列出了控制 i-cms 模式的命令行选项。"推荐的选项"部分建议了一组初始选项。
表 8-1 i-cms 的命令行选项
| 选项 | 描述 | 默认值 (Java SE 5 及更低版本) | 默认值 (Java SE 6 及更高版本) |
|---|---|---|---|
-XX:+CMSIncrementalMode | 启用增量模式。请注意,也必须启用 CMS 收集器(使用 -XX:+UseConcMarkSweepGC),此选项才能起作用。 | disabled | disabled |
-XX:+CMSIncrementalPacing | 启用自动定步。增量模式占空比根据 JVM 运行时收集的统计信息自动调整。 | disabled | disabled |
-XX:CMSIncrementalDutyCycle=<N> | 允许 CMS 收集器运行次要收集之间的时间百分比(0 到 100)。如果 CMSIncrementalPacing 启用,则这只是初始值。 | 50 | 10 |
-XX:CMSIncrementalDutyCycleMin=<N> | CMSIncrementalPacing 启用时,占空系数下限的百分比(0 到 100)。 | 10 | 0 |
-XX:CMSIncrementalSafetyFactor=<N> | 计算占空比时用于增加保守性的百分比(0 到 100)。 | 10 | 10 |
-XX:CMSIncrementalOffset=<N> | 在次要收集之间的时间段内,增量模式占空比向右移动的百分比(0 到 100)。 | 0 | 0 |
-XX:CMSExpAvgFactor=<N> | 计算 CMS 收集统计信息的指数平均值时,用于加权当前样本的百分比(0 到 100)。 | 25 | 25 |
推荐选项
要在 Java SE 8 中使用 i-cms,请使用以下命令行选项:
-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps前两个选项分别启用 CMS 收集器和 i-cms。不需要最后两个选项。它们只是使有关垃圾收集的诊断信息写入标准输出,因此可以看到垃圾收集行为并在以后进行分析。
对于 Java SE 5 和更早版本,Oracle 建议使用以下内容作为 i-cms 的初始命令行选项集:
-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps \
-XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0 -XX:CMSIncrementalDutyCycle=10对于 Java SE 8,建议使用相同的值,尽管控制 i-cms 自动起搏的三个选项的值已成为 Java SE 6 的默认值。
基本故障排除
i-cms 自动调整功能使用程序运行时收集的统计信息来计算占空比,以便并发收集在堆变满之前完成。但是,过去的行为并不是未来行为的完美预测,并且估计值可能并不总是足够准确以防止堆变满。如果出现了太多的完整集合,请尝试一次在 表 8-2“对 i-cms 自动起步功能进行故障排除” 中的步骤。
表 8-2 对 i-cms 自动起步功能进行故障排除
| 步骤 | 选项 |
|---|---|
| 1. 增加安全系数。 | -XX:CMSIncrementalSafetyFactor=<N> |
| 2. 增加最小占空比。 | -XX:CMSIncrementalDutyCycleMin=<N> |
| 3. 禁用自动起搏,并使用固定的占空比。 | -XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N> |
测量
例 8-1"CMS 收集器的输出” 是 CMS 收集器 的输出,带有选项 -verbose:gc 和 -XX:+PrintGCDetails,删除了一些小细节。请注意,CMS 收集器的输出已散布在次要收集器的输出中。通常在并发收集周期中会发生许多次要收集。CMS-initial-mark 指示并发收集周期的开始,CMS-concurrent-mark 指示并发标记阶段的结束,而 CMS-concurrent-sweep 指示并发清除阶段的结束。CMS-concurrent-preclean 表示预清洁阶段,以前没有讨论过。预清洁代表可以在准备重新标记阶段 CMS-mark 的同时执行的工作。最终阶段由 CMS-concurrent-reset 指示,并且正在准备下一个并发收集。
示例 8-1 CMS 收集器的输出
[GC [1 CMS-initial-mark: 13991K(20288K)] 14103K(22400K), 0.0023781 secs]
[GC [DefNew: 2112K->64K(2112K), 0.0837052 secs] 16103K->15476K(22400K), 0.0838519 secs]
...
[GC [DefNew: 2077K->63K(2112K), 0.0126205 secs] 17552K->15855K(22400K), 0.0127482 secs]
[CMS-concurrent-mark: 0.267/0.374 secs]
[GC [DefNew: 2111K->64K(2112K), 0.0190851 secs] 17903K->16154K(22400K), 0.0191903 secs]
[CMS-concurrent-preclean: 0.044/0.064 secs]
[GC [1 CMS-remark: 16090K(20288K)] 17242K(22400K), 0.0210460 secs]
[GC [DefNew: 2112K->63K(2112K), 0.0716116 secs] 18177K->17382K(22400K), 0.0718204 secs]
[GC [DefNew: 2111K->63K(2112K), 0.0830392 secs] 19363K->18757K(22400K), 0.0832943 secs]
...
[GC [DefNew: 2111K->0K(2112K), 0.0035190 secs] 17527K->15479K(22400K), 0.0036052 secs]
[CMS-concurrent-sweep: 0.291/0.662 secs]
[GC [DefNew: 2048K->0K(2112K), 0.0013347 secs] 17527K->15479K(27912K), 0.0014231 secs]
[CMS-concurrent-reset: 0.016/0.016 secs]
[GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs]相对于次要收集暂停时间,初始标记暂停通常较短。并发阶段(并发标记、并发预清理和并发扫描)通常持续的时间明显长于次要收集暂停,如 示例 8-1"CMS 收集器的输出”所示。但是请注意,在这些并发阶段中不会暂停应用程序。重新标记停顿的长度通常可与次要收集相媲美。重新标记暂停受某些应用程序特性的影响(例如,对象修改率高可能会增加此暂停)以及自上次次要收集以来的时间(例如,年轻代中的更多对象可能会增加此暂停)。
说明:
- 本文档内容基于较早期的 JDK 文档(涉及 Java SE 5/6/8)。CMS 收集器在 JDK 9 中被标记为废弃(Deprecated),并在 JDK 14 中被正式移除。
- 文中提到的“永久代(Permanent Generation)”在 JDK 8 中仍存在,但在 JDK 9 及以后已被“元空间(Metaspace)”取代。
- 文中所述的“增量模式(i-cms)”在现代 JDK 版本中已不再支持。对于低延迟需求,建议参考 JDK 11+ 提供的 ZGC 或 Shenandoah 收集器。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/concurrent-mark-sweep-cms-collector.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。