CMS垃圾回收器

105次阅读
没有评论

共计 5588 个字符,预计需要花费 14 分钟才能阅读完成。

 

概述

CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动JVM参数加上-XX:+UseConcMarkSweepGC,这个参数表示对于老年代的回收采用CMS。CMS采用的基础算法是:标记—清除

CMS回收过程

参见代码演示GC回收

CMS是基于“标记-清除”算法实现的,整个过程分为7个步骤

  1. 初始标记(STW)
  2. 并发标记
  3. 并发预清理
  4. 可中断并发预清理
  5. 重标记(STW)
  6. 并发清理
  7. 重置

初始标记

该阶段进行可达性分析,标记GC ROOT能直接关联到的对象。这里的GC root关联的对象包含虚拟机栈中引用的对象、类静态属性引用的对象、本地方法栈中JNI引用的对象。另外,CMS是老年代的垃圾收集器,被新生代引用的对象应该被标记为存活,所以这里还包含新生代对象。
例如:

public class Client {
public static void main(String[] args) {
Master master = new Master();
Subject subject = Master.addSubject(new Subject());
subject.publishEvent();
}
}
public class Master {
public static Subject addSubject(Subject subject) {
ObserverOne observer = new ObserverOne();
subject.addObserver(observer);
return subject;
}
}

上述代码中 ObserverOne对象就不是GC ROOT直接关联的对象,直接关联的对象只有Master

并发标记

该阶段进行GC ROOT TRACING,在整个过程中耗时最长,第一个阶段被暂停的线程(STW)重新开始运行。由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。
该阶段使用三色遍历法遍历标记老年代的所有对象。
三色表示

  • 黑色对象:自己被标记,且引用对象已经处理完成。
  • 灰色对象:自己被标记,但引用对象未处理。
  • 白色对象:没有对它做标记。

标记过的意思就是认为这个对象是存活的,本次GC不回收这个对象。
CMS采用了线性遍历
引入一个bit数组,用它表示内存中每个位置的状态,每个bit位代表4字节的内存空间,所以它的大小是固定的。
然后遍历bitmap,找到被标记为存活的对象cur,将它压入栈中,然后开始遍历这个对象出发的所有对象。遍历栈的过程中,如果遇到地址比cur低的对象则标记并压如栈中,遇到地址比cur高则只标记不入栈。所有GC ROOT引用的对象已经在初始标记阶段标记成了存活对象,遍历过程遇到其中一个就开始利用栈遍历它及它之前的所有对象,这就保证了栈中最多有1个GC root直接引用的对象,有效控制了栈空间的大小。
因为该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。
标记过程举例

CMS垃圾回收器

CMS垃圾回收器

步骤a:对a引用的对象bc,及b引用的对象e进行标记,根据当时的对象引用关系,abcegd是存活的。
步骤b:对象引用关系发生了改变,b不再引用c;新增引用d;g不再引用d
步骤c:完成了并发标记的过程,abceg被标记。第二个和第四个区域内的对象引用关系发生了改变,被记录了下来。这里面运用到了card table、mod union table数据结构和write barrier技术,详见下一节。
步骤d:实际上是重新标记后的结果,可以看到对象d在并发标记结束时未进行标记,但是它还在被对象b引用,不应该回收。这就依赖重新标记阶段对dirty card(对象引用关系发生变化的区域)的处理。
并发过程中变化的维护
JVM将内存分成一个个固定大小的card,然后有一个专门的数据结构(即这里的Card Table)维护每个Card Page的状态,一个字节对应一个Card。当一个Card上的对象的引用发生变化的时候,就将这个Card对应的Card Table上的状态置为dirty(实际上使用的是mod-union table)。之后在预清理和remark阶段会处理这些dirty card

CMS垃圾回收器

CMS垃圾回收器

card table是一个数组,数组中每个位置存的是一个字节(byte),每个比特位有不同的作用。CMS将老年代的空间分成大小为512bytes的块(一个CardPage大小为512bytes),card table中的每个元素对应着一个块。
对于新生代:它记录老年代到新生代的引用,Minor GC时不用遍历整个老年代
对于老年代:它记录并发标记开始引用发生变化的card,并发标记结束后需要处理这些card
由于新生代GC与老年代GC同时使用card table,所以会出现冲突的情况。新生代GC时,发现老年代的dirty card(card的一种状态)没有指向新生代的引用,会将这个card设置为clean(改变了老年代对象引用发生设置的状态),但这个card必须在remark阶段进行重新标记。所以增加了另一个数据结构mod union table解决此问题。
mod union table是一个bit位向量,一个bit表示一个card的状态。它由新生代垃圾收集器维护,新生代GC将card设置为clean之前,把mod union table设置为dirty。card table状态为dirty、或者mod union table标记为dirty、或者同时两种数据结构都标记为dirty的card表示并发标记阶段引用发生了变化,需要在后面的阶段进行处理。
write barrie写屏障类似于一个切面,用户线程写对象引用的时候就触发write barrier的逻辑,将对象所处的card设置为dirty。

并发预清理

通过参数 XX:-CMSPrecleaningEnabled关闭选择关闭该阶段,默认启用,主要做两件事情:

  1. 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
  2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty,通过扫描这些Table,重新标记那些在并发标记阶段引用(Dirty Card)被更新的对象

可中断预清理

本阶段尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world。在该阶段,主要循环的做两件事:

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

避免连续两次暂停导致总的暂停时间过长,其中运用了一些策略。预清理阶段结束之后,如果Eden空间大于CMSScheduleRemarkEdenSizeThreshold(默认2M),则进入可中断预清理阶段。当Eden空间达到CMSScheduleRemarkEdenPenetration(默认50%)时进入remark阶段。如果等待超过了CMSMaxAbortablePrecleanTime(默认5s)同样进入remark阶段。另外,还有个CMSMaxAbortablePrecleanLoops参数可以控制可中断预清理循环的次数,到达次数则退出预清理阶段进入remark,默认是0不限制次数。
-XX:CMSScheduleRemarkEdenSizeThreshold =n 设置可中断预清理触发条件,
-XX:CMSScheduleRemarkEdenPenetration=n 设置中断的条件
-XX:CMSMaxAbortablePrecleanTime=n,设置可中断预清理阶段最长持续时间,单位为s,默认值5s。
-XX:CMSMaxAbortablePrecleanLoops=n 设置该阶段的循环次数(默认0,表示不限制次数)

重新标记

暂停用户线程,从GC root(包含新生代对象)出发重新标记,并处理完所有dirty card。
因为预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。因此,需要有一个stop-the-world的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。主要目的是重新扫描之前并发处理阶段的所有残留更新对象
主要工作:

  • 遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
  • 根据GC Roots,重新标记;
  • 遍历老年代的Dirty Card,重新标记。

重新标记阶段需要遍历新生代对象,但新生代里大多都是垃圾,如果remark之前发生一次新生代GC,则会大大减小remark阶段需要遍历的对象数量。可以设置CMSScavengeBeforeRemark参数强制在remark之前执行一次新生代GC。但是新生代GC也是有停顿的,尤其在新生代对象很少的情况下触发YGC,最严重的是如果在可中断清理阶段已经发生了一次YGC,然后在该阶段又触发一次,会增加停顿时长。
-XX:+CMSScavengeBeforeRemark,强制hotspot虚拟机在cms remark阶段之前做一次minor gc,用于提高remark阶段的速度;

并发清理

并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间

并发重置

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

CMS的缺点

吞吐量低

并发意味着多线程抢占CPU资源,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。CMS默认的回收线程数是**(CPU个数+3)/4。**这个公式的意思是当CPU大于4个时,保证回收线程占用至少25%的CPU资源,这样用户线程占用75%的CPU,这是可以接受的。按照上面的公式,CMS会启动1个GC线程。相当于GC线程占用了50%的CPU资源,这就可能导致用户程序的执行速度忽然降低了50%,50%已经是很明显的降低了。

浮动垃圾

并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。

内存碎片

CMS使用标记清除算法,收集结束之后会产生大量内存碎片。当有大对象需要分配空间时,可能总的空间大小是足够的,但是没有连续的空间装下此对象。
CMS的解决方案是使用UseCMSCompactAtFullCollection参数(默认开启),在顶不住要进行Full GC时开启内存碎片整理。这个过程需要STW,碎片问题解决了,但停顿时间又变长了。
虚拟机还提供了另外一个参数CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认为0,每次进入Full GC时都进行碎片整理)。

Concurrent Mode Failure

CMS需要预留出空间提前开始GC,预留的空间供并发期间新对象的分配及新生代对象的晋升使用CMSInitiatingOccupancyFraction参数来设置老年代空间使用百分,达到百分比就进行垃圾回收。这个参数默认是92%,参数选择需要看具体的应用场景。
设置的太小会导致频繁的CMS GC,产生大量的停顿;设现在设置为99%,还剩1%的空间可以使用。在并发清理阶段,若用户线程需要使用的空间大于1%,就会产生Concurrent Mode Failure错误,意思就是说并发模式失败。这时,虚拟机就会启动备案:使用Serial Old收集器重新对老年代进行垃圾回收。如此一来,停顿时间变得更长。所以CMSInitiatingOccupancyFraction的设置要具体问题具体分析。
-XX:CMSInitiatingOccupancyFraction=70 和-XX:+UseCMSInitiatingOccupancyOnly这两个设置一般配合使用,一般用于降低CMS GC频率或者增加频率、减少GC时长
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);
-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.
-XX:+CMSScavengeBeforeRemark在CMS GC前启动一次Minor GC,目的在于减少老年代对新生代的引用,降低remark时的开销

触发时机

  1. 老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开
  2. 老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
  3. 新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足

正文完
 
yangleduo
版权声明:本站原创文章,由 yangleduo 2023-04-11发表,共计5588字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。