Concurrent Mark Sweep (CMS) Collector (oracle.com)
并发标记清楚(CMS)收集器是为那些喜欢较短的垃圾收集暂停时间,并且能够在应用程序运行时与垃圾收集器共享处理器资源的应用程序设计的。通常情况下,那些拥有相对较大的长寿命数据集(一个大的任期代)并在有两个或更多处理器的机器上运行的应用程序往往从使用这种收集器中受益。然而,任何对暂停时间要求不高的应用程序都应该考虑使用这种收集器。CMS收集器是通过命令行选项-XX:+UseConcMarkSweepGC启用的。
与其他可用的收集器类似,CMS收集器是世代相传的;因此,年轻代收集和年老代收集都会发生。CMS收集器试图通过使用单独的垃圾收集器线程来追踪可达的对象,并与应用程序线程的执行同时进行,从而减少由于主要收集而导致的暂停时间。在每个主要的收集周期中,CMS收集器会在收集开始时短暂地暂停所有的应用线程,并在收集的中期再次暂停。第二次暂停往往是这两次暂停中较长的一次。在两次暂停期间,多个线程被用来做收集工作。剩余的收集工作(包括对实时对象的大部分追踪和对无法到达的对象的清扫)是由一个或多个与应用程序同时运行的垃圾收集器线程完成。年轻代收集可以与正在进行的年老代收集交错进行,并且以类似于并行垃圾收集器的方式进行(特别是在年轻代收集期间,应用线程被停止)。
并发模式失败
CMS收集器使用一个或多个垃圾收集器线程,这些线程与应用线程同时运行,目标是在任期代满之前完成收集。如前所述,在正常运行中,CMS收集器在应用线程仍在运行的情况下完成大部分的跟踪和清扫工作,因此应用线程只看到短暂的暂停。然而,如果CMS收集器无法在年老代填满之前完成对不可达对象的回收,或者如果年老代中的可用空间块无法满足分配,那么应用程序就会暂停,并且在所有应用程序线程停止的情况下完成收集。无法并发完成收集被称为并发模式失败,表明需要调整CMS收集器的参数。如果并发收集被显式垃圾收集(System.gc())或为诊断工具提供信息所需的垃圾收集打断,那么就会报告并发模式中断。
过多的GC时间和OutOfMemoryError
如果垃圾收集时间过长,CMS收集器会抛出OutOfMemoryError:如果垃圾收集的时间超过总时间的98%,而恢复的堆不到2%,那么会抛出OutOfMemoryError。这个功能是为了防止应用程序在长时间运行的同时,由于堆太小,几乎没有进展。如果有必要,可以通过在命令行中添加选项-XX:-UseGCOverheadLimit来禁用该功能。
该策略与并行收集器中的策略相同,只是执行并发收集的时间不计入98%的时间限制。换句话说,只有在应用程序停止时进行的收集才会被计入过多的GC时间。这种收集通常是由于并发模式失败或明确的收集请求(例如,对System.gc的调用)。
浮动垃圾
CMS收集器,就像Java HotSpot VM中的所有其他收集器一样,是一个跟踪收集器,它至少可以识别堆中的所有可达对象。用Richard Jones和Rafael D. Lins在他们的出版物Garbage Collection中的说法。用Richard Jones和Rafael D. Lins在他们的出版物Garbage Collection: Algorithms for Automated Dynamic Memory中的说法,它是一个递增的更新收集器。由于应用程序线程和垃圾收集器线程在主要的收集过程中同时运行,被垃圾收集器线程追踪的对象随后可能在收集过程结束时变得不可及。这种尚未被回收的无法到达的对象被称为浮动垃圾。浮动垃圾的数量取决于并发收集周期的持续时间和应用程序的引用更新频率,也被称为突变。此外,由于年轻代和年老代是独立收集的,每一个都是另一个的根基来源。作为一个粗略的指导方针,尝试将年老代的大小增加20%,以考虑到浮动垃圾。在一个并发的收集周期结束时,堆中的浮动垃圾在下一个收集周期中被收集。
暂停
在一个并发的收集周期中,CMS收集器会暂停一个应用程序两次。第一次暂停是将从根部直接到达的对象(例如,来自应用程序线程堆栈和寄存器的对象引用,静态对象等)和来自堆中其他地方的对象(例如,年年轻代)标记为活的。这第一个停顿被称为初始标记停顿。第二个暂停是在并发追踪阶段结束时出现的,它可以找到并发追踪所遗漏的对象,这些对象是在CMS收集器完成对某个对象的追踪后,由于应用线程对该对象的引用进行更新而遗漏的。这第二个暂停被称为重新标记暂停。
并发阶段
可达对象图的并发追踪发生在初始标记暂停和重新标记暂停之间。在这个并发追踪阶段,一个或多个并发的垃圾收集器线程可能正在使用处理器资源,而这些资源本来是可以提供给应用程序的。因此,在这个阶段和其他并发阶段,即使应用程序线程没有暂停,计算型的应用程序可能会看到应用程序吞吐量相应的下降。在重新标记暂停之后,一个并发的清扫阶段会收集被确认为不可达的对象。一旦一个收集周期完成,CMS收集器就会等待,几乎不消耗计算资源,直到下一个主要收集周期的开始。
开始一个并发的收集周期
在 serial 收集器中,只要保证年轻代已满,就会发生垃圾回收,并且在收集完成时,所有的应用线程都会停止。相比之下,并发收集的开始必须在时间上保证收集能在年老代满之前完成;否则,应用程序将观察到由于并发模式失败而导致的较长时间的暂停。有几种方法来启动一个并发的收集。
根据最近的历史记录,CMS收集器保持着对 "年老代代 "耗尽前的剩余时间以及对并发收集周期所需时间的估计。利用这些动态的估计值,一个并发的收集周期被启动,目的是在保持年老代被耗尽之前完成收集周期。为了安全起见,这些估计值是加了比重的,因为并发模式的失败会造成很大的损失。
如果老年代的占用率超过了启动占用率(年老代的一个百分比),也会启动并发收集。这个启动占用率阈值的默认值约为92%,但这个值会随着版本的变化而变化。这个值可以通过命令行选项-XX:CMSInitiatingOccupancyFraction=<N>来手动调整,其中<N>是保质期生成量的一个整体百分比(0到100)。
调度停顿
年轻代收集的暂停和年老代收集的暂停独立发生。它们不会重叠,但可能会快速连续发生,这样一来,一个收集的暂停,紧接着另一个收集的暂停,就会显得是一个单一的、较长的暂停。为了避免这种情况,CMS收集器试图在前一个和后一个年轻代的暂停之间安排重新标记暂停,大致在中间。目前,对于最初的标记停顿没有这样的安排,它通常比重新标记停顿短得多。
增量模式
请注意,增量模式在 Java SE 8 中被弃用,并可能在未来的主要版本中被删除。
CMS收集器可以在一种模式下使用,其中并发阶段是以增量方式完成的。回顾一下,在并发阶段,垃圾收集器线程正在使用一个或多个处理器。增量模式的目的是通过定期停止并发阶段,把处理器还给应用程序,来减少长的并发阶段的影响。这种模式,在这里被称为i-cms,将收集器并发的工作分成小块时间,安排在年轻代收集之间。当需要CMS采集器提供的低暂停时间的应用程序在处理器数量较少的机器上运行时(例如,1或2个),这个功能很有用。
并发收集周期通常包括以下步骤。
1-停止所有的应用线程,确定从根部可到达的对象集,然后恢复所有的应用线程。
2-在应用线程执行时,使用一个或多个处理器,并发地追踪可达对象图。
3-使用一个处理器,同时回溯自上一步追踪以来被修改的对象图的部分。
4-停止所有应用线程,回溯根和对象图中自上次检查后可能被修改的部分,然后恢复所有应用线程。
5-同时使用一个处理器,将无法到达的对象清扫到用于分配的自由列表中。
6-同时调整堆的大小,为下一个收集周期准备支持数据结构,使用一个处理器。
通常情况下,CMS收集器在整个并发标记阶段使用一个或多个处理器,而不会主动放弃这些处理器。同样地,在整个并发清除阶段也使用一个或多个处理器,同样没有放弃它。这种开销对于有响应时间限制的应用来说,可能会造成太大的干扰,否则可能会使用处理核心,特别是在只有一个或两个处理器的系统上运行。增量模式解决了这个问题,它将并发的阶段分解成短时间的活动,这些活动被安排在小停顿的中间位置。
i-cms模式使用一个占空比来控制CMS收集器在自愿放弃处理器之前被允许做的工作量。占空比是允许CMS采集器在年轻一代采集之间运行的时间百分比。i-cms模式可以根据应用程序的行为自动计算占空比(推荐的方法,称为自动起搏),也可以在命令行中把占空比设置为一个固定值。
**命令行选项 **
控制i-cms模式的命令行选项。推荐选项
| Option | Description | Default Value, Java SE 5 and Earlier | Default Value, Java SE 6 and Later |
|---|---|---|---|
| -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
尽管控制i-cms自动步调的三个选项的值在JavaSE6中变成了默认值,但对于JavaSE8来说,建议使用同样的值。
基本故障排除
i-cms的自动步调功能使用程序运行时收集的统计数据来计算占空比,以便在堆变满之前完成并发收集。然而,过去的行为并不能完美地预测未来的行为,估计的结果可能并不总是足够准确,以防止堆变满。如果出现了太多的满载集合,那么请尝试表下表中的步骤,"i-cms自动步调功能的故障排除",一次一个。
i-cms自动计步功能的故障排除
| Step | Options |
|---|---|
| 1. 增加安全系数。 | -XX:CMSIncrementalSafetyFactor=<N> |
| 2. 增加最小占空比。 | -XX:CMSIncrementalDutyCycleMin=<N> |
| 3. 禁用自动起搏,使用固定占空比。 | -XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N> |
测量结果
下图是CMS采集器的输出,选项为-verbose:gc和-XX:+PrintGCDetails,并删除了一些其他的细节。请注意,CMS采集器的输出是与其他采集的输出穿插在一起的;通常在一个同时进行的采集周期中会发生许多次要采集。
CMS-initial-mark表示并发收集周期的开始,
CMS-concurrent-mark表示并发标记阶段的结束,
CMS-concurrent-sweep表示并发清扫阶段的结束。
前面没有讨论的是由CMS-concurrent-preclean表示的预清扫阶段。预清扫表示可以同时进行的工作,为重新标记阶段的CMS-remark做准备。最后一个阶段是由CMS-concurrent-reset表示的,是为下一次并发采集做准备。
[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
]
相对于整个年轻代收集的暂停时间,初始标记暂停时间通常很短。并发进行的阶段(同时进行的标记、同时进行的预清洁和同时进行的清扫)通常比年轻代的暂停时间长得多,如例上图所示。然而,请注意,在这些并发阶段,应用程序并没有暂停。重新标记暂停的长度通常与年轻代采集的长度相当。重新标记暂停受到某些应用程序特性的影响(例如,对象的高修改率会增加这个暂停),以及自上次年轻代收集以来的时间(例如,年轻代的更多对象会增加这个暂停)。