当前位置:   article > 正文

JVM垃圾回收器-CMS并发标记清除_cms 并发预清理

cms 并发预清理

Java8的CMS垃圾回收器官方文档参考:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector

一、CMS简介

并发标记清除(Concurrent Mark Sweep, CMS)收集器是为那些喜欢更短的垃圾收集暂停时间,并且能够在应用程序运行时与垃圾收集器共享处理器资源的应用程序设计的
通常,具有相对较大的长寿命数据集(大的年老代)并运行在具有两个或更多处理器的机器上的应用程序往往会受益于使用这种收集器。但是,对于任何暂停时间要求较低的应用程序,都应该考虑使用此收集器。
CMS收集器通过命令行选项启用-XX:+UseConcMarkSweepGC

与其他可用收集器类似,CMS收集器是分代的;因此,minor collection和major collection同时发生。CMS收集器试图通过使用单独的垃圾收集器线程在执行应用程序线程的同时跟踪可到达的对象,从而减少由于主要收集而导致的暂停时间。在每个主要的收集周期中,CMS收集器在收集开始时暂停所有应用程序线程一小段时间,然后在收集中期再次暂停。第二个停顿往往是两个停顿中较长的一个。在两次暂停期间使用多个线程执行收集工作。收集的其余部分(包括活动对象的大部分跟踪和不可到达对象的清除)是通过一个或多个与应用程序同时运行的垃圾收集器线程完成的。次要收集可以与正在进行的主循环交织,并且以类似于并行收集器的方式完成(特别是,在次要收集期间停止应用程序线程)。

二、CMS两种模式,一种压缩策略,一种OOM机制

  1. Backgroud CMS (正常进行的CMS)

    (1)初始标记 CMS initial mark     标记GC Roots直接相关联的第一个对象,不用Tracing,速度很快 STW (jdk7默认单线程,jdk8默认多线程-XX:+CMSParallelInitialMarkEnabled=true)
    (2)并发标记 CMS concurrent mark  进行GC Roots Tracing,找出初始标记对象的所有的引用链上的剩余对象 (多线程并发执行,GC线程和应用的业务线程同时存在)
    (3)重新标记 CMS remark           修改并发标记因用户程序变动的内容 (前期多线程末尾执行STW)
    (4)并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾 (多线程并发执行,GC线程和应用的业务线程同时存在)
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

  2. Foregroud CMS (并发模式失败的CMS)(见第三部分)

  3. CMS的标记压缩算法MSC(Mark Sweep Compact)

    // 开启在FULL GC 之后进行内存压缩,MSC使用的是`Lisp2滑动整理算法`
    -XX:+UseCMSCompactAtFullCollection (java8 默认开启,java 11没有此配置)
    // 0表示每次Full GC 之后都会压缩  (可以配置高一点,比如:5)
    -XX:CMSFullGCsBeforeCompaction=0(java8 默认0,java 11没有此配置)
    
    • 1
    • 2
    • 3
    • 4
  4. OOM机制(见第四部分)

三、并发模式失败 Concurrent Mode Failure

CMS收集器使用一个或多个垃圾收集器线程,这些线程与应用程序线程同时运行,目标是在年老代满之前完成年老代的收集。如前所述,在正常操作中,CMS收集器在应用程序线程仍然运行的情况下执行大部分跟踪和清理工作,因此应用程序线程只能看到短暂的暂停。但是,如果CMS收集器无法在年老代填满之前完成回收不可到达的对象,或者如果年老代中的可用空闲空间块不能满足分配,那么应用程序将暂停,并在停止所有应用程序线程的情况下完成收集(STW)。不能同时完成收集被称为并发模式失败,这表明需要调整CMS收集器参数。如果并发收集被显式垃圾收集(System.gc())或为诊断工具提供信息所需的垃圾收集中断,则报告并发模式中断。

调整CMS收集器参数:

// 开启时,只有当老年代的内存使用达到设置的百分比时才会触发CMS垃圾回收
-XX:CMSInitiatingOccupancyFraction  (默认-1不启用,默认使用CMSTriggerRatio等参数计算比例)
// 只使用CMSInitiatingOccupancyFraction参数设定的回收阈值
-XX:+UseCMSInitiatingOccupancyOnly  (默认false
  • 1
  • 2
  • 3
  • 4

默认的老年代触发CMS垃圾回收的百分比计算公式:

((100 - MinHeapFreeRatio) + (double)( CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0
  • 1

在这里插入图片描述
在这里插入图片描述

Java8的CMSTriggerRatio 默认80,MinHeapFreeRatio默认值40

((100 - 40) + (double)( 80 * 40) / 100.0) / 100.0 = 0.92
  • 1

占比92%(比较容易出现并发模式失败),需要调整参数。

// 比如设置占比为70%触发CMS
-XX:CMSInitiatingOccupancyFraction=70,去控制当老年代的内存占用达到70%的时候,便开启CMS回收年老代。
  • 1
  • 2

四、GC超时和内存溢出 Excessive GC Time and OutOfMemoryError

如果在垃圾收集上花费的时间太多,CMS收集器就会抛出一个OutOfMemoryError:如果花费在垃圾收集上的总时间超过98%,而回收堆的时间少于2%,那么就会抛出一个OutOfMemoryError。 此特性旨在防止应用程序运行一段较长的时间,而由于堆太小而几乎没有任何进展。如果需要,可以通过在命令行中添加选项-XX:-UseGCOverheadLimit来禁用该特性(默认开启)

该策略与并行收集器中的策略相同,只是执行并发收集的时间不计入98%的时间限制。换句话说,只有在应用程序停止时执行的收集才会导致过多的GC时间。这样的集合通常是由于并发模式失败或显式的垃圾回收请求(例如,对System.gc的调用)造成的

五、浮动垃圾 Floating Garbage

CMS收集器与Java HotSpot VM中的所有其他收集器一样,是一个跟踪收集器,它至少标识堆中所有可到达的对象。用Richard Jones和Rafael D. Lins在他们发表的《垃圾收集:自动动态内存算法》中的话说,它是一个增量更新收集器(incremental update collector)由于应用程序线程和垃圾收集器线程在主要收集期间并发运行,垃圾收集器线程跟踪的对象可能在收集过程结束时变得不可访问。这种尚未被回收的不可达对象称为浮动垃圾

浮动垃圾的数量取决于并发收集周期的持续时间和应用程序的引用更新(也称为突变)的频率。此外,由于年轻代和年老代是独立收集的,每一个都是另一个的根的来源。作为一个粗略的指导方针,尝试增加20%的年老代的大小,以占浮动垃圾的比例。在一个并发收集周期结束时,将在下一个收集周期期间收集堆中的浮动垃圾

漏标:

漏标会导致被引用的对象被当成垃圾误删除,增量更新是解决漏标问题的一种方式,G1使用原始快照(Snapshot At The Beginning,SATB)的方式处理
  • 1

漏标只有同时满足以下两个条件时才会发生:

条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。

条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。
  • 1
  • 2
  • 3

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)

以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

写屏障实现原始快照(SATB): 当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:

写屏障实现增量更新: 当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D 记录下来:

写前屏障,在等号赋值之前,将Object.filed原有的引用对象记录下来(相当于排个快照,SATB)
Object.filed = New_Object
写后屏障,在等号赋值之后,将新的引用对象New_Object记录下来
  • 1
  • 2
  • 3

六、停顿 Pauses

CMS收集器在并发收集周期中暂停应用程序两次。
第一个暂停是将从根(例如,来自应用程序线程堆栈和寄存器的对象引用、静态对象等)和从堆的其他地方(例如,年轻代)直接可访问的对象标记为活动对象

第一次暂停称为初始标记停顿。第二次暂停出现在并发跟踪阶段的末尾,查找由于CMS收集器完成跟踪对象后应用程序线程更新对象中的引用而导致并发跟踪错过的对象。这第二次停顿称为重新标记停顿

七、并发阶段 Concurrent Phases

可达对象图的并发跟踪发生在初始标记停顿和重新标记停顿之间。在此并发跟踪阶段,一个或多个并发垃圾收集器线程可能正在使用应用程序本来可用的处理器资源。因此,即使应用程序线程没有暂停,计算绑定的应用程序在此和其他并发阶段的应用程序吞吐量也可能会出现相应的下降。在重新标记停顿之后,一个并发清除阶段将收集标识为不可达的对象。一旦一个收集周期完成,CMS收集器就会等待,几乎不消耗任何计算资源,直到下一个主要收集周期开始。

八、开始并发收集周期 Starting a Concurrent Collection Cycle

对于串行收集器,只要年老代已满,并且在收集完成时停止所有应用程序线程,就会发生主收集。相反,并发收集的开始必须定时,以便收集可以在年老代满之前完成;否则,由于并发模式失败,应用程序将观察到更长的暂停。有几种方法可以启动并发收集

CMS收集器根据最近的历史记录,维护年老代耗尽之前剩余时间的估计,以及并发收集周期所需的时间。使用这些动态估计,开始一个并发收集周期,目的是在年老代耗尽之前完成收集周期。这些估计是为了安全而进行的,因为并发模式故障的代价可能非常高。

如果年老代的占用率超过初始占用率(年老代的百分比),也会启动并发收集。这个初始占用阈值的默认值大约是92%(参考第三部分),但是该值会随着版本的不同而变化。该值可以使用命令行选项手动调整-XX: cmsinitiatingocupancyfraction =,其中是年老代大小的整数百分比(0到100)。

九、调度停顿 Scheduling Pauses

年轻代收集和年老代收集的停顿是独立发生的。它们不重叠,但可以快速连续地出现,这样一个集合的停顿,紧接着是另一个集合的停顿,看起来可能是一个单独的、更长的停顿。
为了避免这种情况,CMS收集器尝试在上一代和下一代年轻代停顿之间安排重新标记停顿。目前还没有对初始标记停顿执行此调度,初始标记停顿通常比重新标记停顿短得多

十、增量模式 Incremental Mode

请注意,增量模式在Java SE 8中已被弃用,并可能在未来的主要版本中被删除。

CMS收集器可以在并发阶段逐步完成的模式中使用。回想一下,在并发阶段,垃圾收集器线程正在使用一个或多个处理器。增量模式旨在通过定期停止并发阶段,将处理器交还给应用程序,从而减少长并发阶段的影响。这种模式在这里称为i-cms,它将收集器并发完成的工作划分为在年轻代收集之间调度的小时间块。当需要CMS收集器提供的低停顿时间的应用程序运行在具有少量处理器(例如,1或2)的机器上时,此特性非常有用

并发收集周期通常包括以下步骤:

  • 停止所有应用程序线程,标识从根可达的对象集,然后恢复所有应用程序线程。(STW 初始标记)

  • 在应用程序线程执行时,使用一个或多个处理器并发跟踪可达的对象图。(并发标记)

  • 使用一个处理器,同时回溯自上一步跟踪以来被修改的对象图的部分。(重新标记)

  • 停止所有应用程序线程,并回溯根和对象图中自上次检查以来可能被修改的部分,然后恢复所有应用程序线程。(STW重新标记的末尾停顿,确定本次需要清除的垃圾对象)

  • 使用一个处理器,将不可达的对象并发清除到用于分配的空闲列表中。(并发清除)

  • 使用一个处理器并发地调整堆的大小,并为下一个收集周期准备支持数据结构。

通常,CMS收集器在整个并发跟踪阶段使用一个或多个处理器,而不会主动放弃它们。类似地,一个处理器用于整个并发清除阶段,同样不放弃它。对于响应时间受限的应用程序来说,这种开销可能会造成很大的干扰,这些应用程序可能会使用处理核心,特别是在只有一个或两个处理器的系统上运行时。增量模式通过将并发阶段分解为活动的短爆发来解决这个问题,这些活动被安排在小暂停之间的中途发生

i-cms模式使用工作周期来控制CMS收集器在自愿放弃处理器之前所允许做的工作量。工作周期是允许CMS收集器运行的年轻代收集之间的时间百分比。i-cms模式可以根据应用程序的行为自动计算工作周期(推荐的方法,称为自动步调),也可以在命令行中将工作周期设置为一个固定值。

十一、命令行选项 Command-Line Options

表8-1“命令行选项for i-cms”列出了控制i-cms模式的命令行选项。推荐选项一节建议了一组初始选项。
表8-1 i-cms命令行选项:
在这里插入图片描述

推荐选项:
要在Java SE 8中使用i-cms,请使用以下命令行选项:

-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode \
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
  • 1
  • 2

前两个选项分别启用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
  • 1
  • 2
  • 3
  • 4

JavaSE8也推荐使用相同的值,不过JavaSE6中控制i-cms自动调速的三个选项的值是默认值。

十二、基本故障排除 Basic Troubleshooting

i-cms自动调整特性使用程序运行时收集的统计信息来计算工作周期,以便在堆被填满之前完成并发收集。然而,过去的行为并不能完美地预测未来的行为,而且估计可能并不总是足够准确,以防止堆被填满。如果出现太多的全收集,请尝试表8-2“i-cms自动步调特性故障排除”中的步骤,一次一个。
表8-2 i-cms自动步调故障处理
在这里插入图片描述

十三、测量指标 Measurements

备注:
	次要收集(minor collection) 通常指的是年轻代垃圾收集
	remark 指重新标记
  • 1
  • 2
  • 3

示例8-1,“CMS收集器的输出”是CMS收集器的输出,带有选项-verbose:gc和-XX:+PrintGCDetails,删除了一些次要细节。注意,CMS收集器的输出与次要集合的输出穿插在一起;
通常,在并发收集周期中会发生许多次要收集(minor collections)。CMS-initial-mark表示并发收集周期的开始,CMS-concurrent-mark表示并发标记阶段的结束,CMS-concurrent-sweep表示并发扫描阶段的结束。
前面没有讨论由CMS-concurrent-preclean表示的预清洗阶段。预清理代表可以在准备重新标记阶段CMS-remark时同时进行的工作。最后一个阶段由CMS-concurrent-reset指示,并为下一次并发收集做准备
示例8-1 CMS Collector的输出信息

// CMS-initial-mark
[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
[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
// -XX:+CMSPrecleaningEnabled(默认开启)
[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
[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
[CMS-concurrent-reset: 0.016/0.016 secs]
[GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

相对于次要收集停顿时间,初始标记停顿通常较短。并发阶段(并发标记、并发预清理和并发扫描)通常比minor collection停顿持续的时间长得多,如示例8-1所示,“CMS收集器的输出”。但是请注意,在这些并发阶段中应用程序没有停顿。重新标记停顿的长度通常与minor collection的长度相当。重新标记停顿受某些应用程序特征的影响(例如,对象修改的高比例可能会增加此停顿),以及自上次次要收集(minor collection)以来的时间(例如,年轻代中更多的对象可能会增加此停顿)。

十四、并发预清理 CMS-concurrent-preclean

CMS-concurrent-preclean并发预清理配置:
在这里插入图片描述
如何判断老年代中的对象有没有被年轻代中的对象引用?(如何判断老年代中的对象是否存活),通过扫描年轻代来确定。但是扫面年轻代,垃圾回收的停顿时间就会变成,所以需要在扫面之前,进行一次young GC(minor collection),回收之后,对象变少了,扫描就快了,但是触发预清理是有条件的:

-XX:CMSScheduleRemarkEdenSizeThreshold 默认值:2M
-XX:CMSScheduleRemarkEdenPenetration 默认值:50%
  • 1
  • 2

Eden空间使用超过2M的时候启动可中断的并发预清理(CMS-concurrent-abortable-preclean),到Eden空间使用率到达50%的时候中断(但不是结束),进入Remark(重新标记阶段)

CMS-concurrent-abortable-preclean可中断的并发预清理并不是一直执行的,是有时间限制的,当超过一定的时间(CMSMaxAbortablePrecleanTime,默认5s)即使Eden空间使用率没有到达50%,一样会停止收集,进入remark阶段。

// 默认不循环
-XX:CMSMaxAbortablePrecleanLoops=0
// 可中断的并发预清理最大执行时间
-XX:CMSMaxAbortablePrecleanTime=5000 
  • 1
  • 2
  • 3
  • 4

如果在5s内,minor GC没有执行,也可以通过CMSScavengeBeforeRemark参数配置,在重新标记之前,强制进行一次Minor GC。

// 默认false,开启使用-XX:+CMSScavengeBeforeRemark
-XX:-CMSScavengeBeforeRemark
  • 1
  • 2

十五、三色标记

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。这里引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:

黑色:

表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。
 黑色的对象代表已经扫描过, 它是安全存活的, 
如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 
黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
  • 1
  • 2
  • 3
  • 4

灰色:

表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
  • 1

白色:

表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 
所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
  • 1
  • 2

标记过程:

1.初始时,所有对象都在 【白色集合】中;

2.将GC Roots 直接引用到的对象 挪到 【灰色集合】中;

3.从灰色集合中获取对象:

4. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
5. 将本对象 挪到 【黑色集合】里面。

重复步骤3.4,直至【灰色集合】为空时结束。

结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/561932
推荐阅读
相关标签
  

闽ICP备14008679号