CMS与三色标记算法


CMS(Concurrent Mark Sweep)是一款里程碑式的垃圾收集器,为什么这么说呢?因为在它之前,GC线程和用户线程是无法同时工作的,即使是Parallel Scavenge,也不过是GC时开启多个线程并行回收而已,GC的整个过程依然要暂停用户线程,即Stop The World。这带来的后果就是Java程序运行一段时间就会卡顿一会,降低应用的响应速度,这对于运行在服务端的程序是不能被接收的。

GC时为什么要暂停用户线程?
首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。
其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。

  1. 漏标
    原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。
  2. 错标
    原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。

针对这些问题,CMS是如何解决的呢?它是如何做到GC线程和用户线程并发工作的呢???

CMS收集器

Concurrent Mark Sweep,从名字上就可以看出来,这是一款采用「标记清除」算法的垃圾收集器,它运行的示意图大概如下:
在这里插入图片描述
大概可分为四个主要步骤:
在这里插入图片描述
1、初始标记
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。初始标记的过程是需要触发STW的,不过这个过程非常快,而且初试标记的耗时不会因为堆空间的变大而变慢,是可控的,因此可以忽略这个过程导致的短暂停顿。

2、并发标记
并发标记就是将初始标记的对象进行深度遍历,以这些对象为根,遍历整个对象图,这个过程耗时较长,而且标记的时间会随着堆空间的变大而变长。不过好在这个过程是不会触发STW的,用户线程仍然可以工作,程序依然可以响应,只是程序的性能会受到一点影响。因为GC线程会占用一定的CPU和系统资源,对处理器比较敏感。CMS默认开启的GC线程数是:(CPU核心数+3)/4,当CPU核心数超过4个时,GC线程会占用不到25%的CPU资源,如果CPU数不足4个,GC线程对程序的影响就会非常大,导致程序的性能大幅降低。

3、重新标记
由于并发标记时,用户线程仍在运行,这意味着并发标记期间,用户线程有可能改变了对象间的引用关系,可能会发生两种情况:一种是原本不能被回收的对象,现在可以被回收了,另一种是原本可以被回收的对象,现在不能被回收了。针对这两种情况,CMS需要暂停用户线程,进行一次重新标记。

4、并发清理
重新标记完成后,就可以并发清理了。这个过程耗时也比较长,且清理的开销会随着堆空间的变大而变大。不过好在这个过程也是不需要STW的,用户线程依然可以正常运行,程序不会卡顿,不过和并发标记一样,清理时GC线程依然要占用一定的CPU和系统资源,会导致程序的性能降低。

CMS的缺点

尽管CMS是一款里程碑式的垃圾收集器,开启了GC线程和用户线程同时工作的先河,但是不管是哪个JDK版本,CMS从来都不是默认的垃圾收集器,究其原因,还是因为CMS不太完美,存在一些缺点。
在这里插入图片描述

1、对处理器敏感
并发标记、并发清理阶段,虽然CMS不会触发STW,但是标记和清理需要GC线程介入处理,GC线程会占用一定的CPU资源,进而导致程序的性能下降,程序响应速度变慢。CPU核心数多的话还稍微好一点,CPU资源紧张的情况下,GC线程对程序的性能影响非常大。

2、浮动垃圾
并发清理阶段,由于用户线程仍在运行,在此期间用户线程制造的垃圾就被称为“浮动垃圾”,浮动垃圾本次GC无法清理,只能留到下次GC时再清理。

3、并发失败
由于浮动垃圾的存在,因此CMS必须预留一部分空间来装载这些新产生的垃圾。CMS不能像Serial Old收集器那样,等到Old区填满了再来清理。在JDK5时,CMS会在老年代使用了68%的空间时激活,预留了32%的空间来装载浮动垃圾,这是一个比较偏保守的配置。如果实际引用中,老年代增长的不是太快,可以通过-XX:CMSInitiatingOccupancyFraction参数适当调高这个值。到了JDK6,触发的阈值就被提升至92%,只预留了8%的空间来装载浮动垃圾。
如果CMS预留的内存无法容纳浮动垃圾,那么就会导致「并发失败」,这时JVM不得不触发预备方案,启用Serial Old收集器来回收Old区,这时停顿时间就变得更长了。

4、内存碎片
由于CMS采用的是「标记清除」算法,这就意味这清理完成后会在堆中产生大量的内存碎片。内存碎片过多会带来很多麻烦,其一就是很难为大对象分配内存。导致的后果就是:堆空间明明还有很多,但就是找不到一块连续的内存区域为大对象分配内存,而不得不触发一次Full GC,这样GC的停顿时间又会变得更长。
针对这种情况,CMS提供了一种备选方案,通过-XX:CMSFullGCsBeforeCompaction参数设置,当CMS由于内存碎片导致触发了N次Full GC后,下次进入Full GC前先整理内存碎片,不过这个参数在JDK9被弃用了。


三色标记算法

介绍完CMS垃圾收集器后,我们有必要了解一下,为什么CMS的GC线程可以和用户线程一起工作。

JVM判断对象是否可以被回收,绝大多数采用的都是「可达性分析」算法,关于这个算法,可以查看笔者以前的文章:大白话理解可达性分析算法

从GC Roots开始遍历,可达的就是存活,不可达的就回收。

CMS将对象标记为三种颜色:在这里插入图片描述
标记的过程大致如下:

  1. 刚开始,所有的对象都是白色,没有被访问。
  2. 将GC Roots直接关联的对象置为灰色。
  3. 遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。
  4. 重复步骤3,直到没有灰色对象为止。
  5. 结束时,黑色对象存活,白色对象回收。

这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。

漏标

假设GC已经在遍历对象B了,而此时用户线程执行了A.B=null的操作,切断了A到B的引用。
在这里插入图片描述
本来执行了A.B=null之后,B、D、E都可以被回收了,但是由于B已经变为灰色,它仍会被当做存活对象,继续遍历下去。
最终的结果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算是浮动垃圾的一部分。

实际上,这个问题依然可以通过「写屏障」来解决,只要在A写B的时候加入写屏障,记录下B被切断的记录,重新标记时可以再把他们标为白色即可。

错标

假设GC线程已经遍历到B了,此时用户线程执行了以下操作:

B.D=null;//B到D的引用被切断
A.xx=D;//A到D的引用被建立

在这里插入图片描述
B到D的引用被切断,且A到D的引用被建立。
此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当做垃圾回收。
可以看到错标的结果比漏表严重的多,浮动垃圾可以下次GC清理,而把不该回收的对象回收掉,将会造成程序运行错误。

错标只有在满足下面两种情况下才会发生:
在这里插入图片描述

只要打破任一条件,就可以解决错标的问题。

原始快照和增量更新

原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。相当于无论引用关系是否删除,都会按照刚开始扫描时那一瞬间的对象图快照来扫描。

增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。

写屏障

这个写屏障指的可不是并发编程里的写屏障哦!这里的写屏障指的是属性赋值的前后加入一些处理,类似于AOP。

CMS采用的方案就是:写屏障+增量更新来实现的,打破的是第二个条件。

当黑色指向白色的引用被建立时,通过写屏障来记录引用关系,等扫描结束后,再以引用关系里的黑色对象为根重新扫描一次即可。

伪代码大致如下:

class A{
	private D d;

	public void setD(D d) {
		writeBarrier(d);// 插入一条写屏障
		this.d = d;
	}

	private void writeBarrier(D d){
		// 将A -> D的引用关系记录下来,后续重新扫描
	}
}

尾巴

CMS为了让GC线程和用户线程一起工作,回收的算法和过程比以前旧的收集器要复杂很多。究其原因,就是因为GC标记对象的同时,用户线程还在修改对象的引用关系。因此CMS引入了三色算法,将对象标记为黑、灰、白三种颜色的对象,并通过「写屏障」技术将用户线程修改的引用关系记录下来,以便在「重新标记」阶段可以修正对象的引用。
虽然CMS从来没有被JDK当做默认的垃圾收集器,存在很多的缺点,但是它开启了「GC并发收集」的先河,为后面的收集器提供了思路,光凭这一点,就依然值得记录下来。

<think>好的,我现在需要回答用户关于G1垃圾回收器中三色标记法的实现原理的问题。首先,我要回顾一下已有的知识,并查看提供的参考资料。 根据引用[1],G1是基于标记-整理算法,但局部使用复制算法,并且采用了Region的内存布局。引用[2]提到CMS和G1在并发回收时使用三色标记法,但存在多标和漏标的问题。引用[3]指出垃圾回收算法的基础,而引用[4]说明主流的收集器使用可达性分析,三色标记法用颜色标记对象状态。 首先,我需要解释三色标记法的基本概念。三色标记法将对象分为白、灰、黑三种颜色,分别表示未扫描、正在扫描和已扫描。在G1中,这个过程是并发进行的,所以需要处理并发时的多标和漏标问题。 接下来,要结合G1的特点。G1将堆分成多个Region,这可能影响标记的过程。并发标记阶段如何用户线程同时运行,这时候如何确保标记的正确性?这里可能会提到SATB(Snapshot-At-The-Beginning)算法,用于处理漏标问题,如引用[2]中提到的。 然后,需要说明G1如何解决漏标和多标。漏标通常通过写屏障和SATB来处理,确保在并发阶段开始时的对象关系快照。多标则可能导致浮动垃圾,但可以通过下一次回收处理。 另外,引用[1]提到G1是局部收集,基于Region的设计,这可能意味着标记过程需要针对不同Region进行优化,可能涉及记忆集(Remembered Set)来记录跨Region引用,但这可能三色标记法本身关联不大,但需要确认。 需要确保术语正确,比如白色是不可达,黑色是处理完的,灰色是待处理的。同时,强调并发阶段的问题及解决方案。 最后,总结G1如何结合三色标记法实现高效的并发回收,并处理标记过程中的问题。同时,可能需要生成相关问题,但用户已要求,所以放在最后。 现在需要组织这些思路,确保逻辑清晰,引用正确的资料,并符合格式要求,如使用中文,数学表达式用$...$,独立公式用$$...$$,但这里可能用不到数学公式,但需要检查是否有需要的地方。例如,解释标记过程可能用流程图或公式,但用户问题可能不需要。 需要避免技术错误,比如确认SATB确实是G1使用的,而CMS可能用增量更新。同时,确保解释清楚写屏障的作用。 最后,检查是否符合所有系统级指令,特别是引用标注的正确位置。</think>### G1垃圾回收器中三色标记法的实现原理 G1(Garbage First)垃圾回收器采用**三色标记法**作为并发标记阶段的核心算法,用于高效识别堆内存中的存活对象。其实现原理可分为以下关键点: --- #### 1. **三色标记法的基本定义** 三色标记法通过颜色标记对象状态,基于可达性分析算法: - **白色**:未被垃圾回收器访问,可能为垃圾对象[^4]。 - **灰色**:对象被访问,但其引用的子对象还未被完全扫描。 - **黑色**:对象及其子对象均被扫描完成,确定为存活对象。 标记过程本质上是**从GC Roots出发**,逐步遍历对象图的广度优先搜索(BFS)过程。 --- #### 2. **G1的并发标记挑战** G1的标记阶段需要用户线程并发执行,这会引发两种问题: - **多标(浮动垃圾)**:标记过程中,用户线程可能切断某些对象的引用,但这些对象已被标记为存活(黑色)。这类垃圾需等待下次回收处理[^2]。 - **漏标(对象消失)**:用户线程修改引用关系,导致存活对象被误标为白色。例如: - 赋值器插入一条从黑色对象到白色对象的引用。 - 赋值器删除所有从灰色对象到该白色对象的引用。 --- #### 3. **G1解决漏标的机制:SATB写屏障** G1采用**SATB(Snapshot-At-The-Beginning)算法**解决漏标问题: - **原理**:在并发标记开始时,对对象引用关系建立快照。即使并发过程中引用关系被修改,快照中的存活对象仍会被标记。 - **实现**:通过**写屏障(Write Barrier)**技术,拦截对象引用修改操作: ```java // 伪代码示例:SATB写屏障 void pre_write_barrier(Field field, Object new_value) { if (current_color(field) == BLACK && new_value == WHITE) { add_to_satb_queue(field); // 记录原引用关系 } } ``` 所有被删除的引用会被记录到SATB队列,后续重新扫描这些引用,确保存活对象不被遗漏。 --- #### 4. **G1的分区(Region)标记优化** G1将堆划分为多个大小相等的Region,标记时针对不同Region采用差异化策略: - **并发标记阶段**:遍历所有Region,但仅标记存活对象。 - **局部回收(Mixed GC)**:根据Region的存活对象比例(垃圾“密度”)优先回收高收益Region[^1]。 --- #### 5. **标记过程的阶段划分** G1的标记过程分为多个阶段以降低停顿时间: 1. **初始标记(Initial Mark)**:暂停用户线程(STW),标记从GC Roots直接可达的对象。 2. **并发标记(Concurrent Mark)**:用户线程并发遍历对象图。 3. **最终标记(Final Mark)**:处理SATB队列中的剩余引用,修正漏标对象。 4. **清理阶段(Cleanup)**:统计Region存活数据,为Mixed GC做准备。 --- ### 总结 G1通过**三色标记法+SATB写屏障**的组合,在并发标记阶段高效识别存活对象,同时解决漏标问题。其面向局部收集的设计Region内存布局进一步优化了标记效率,降低了停顿时间[^4]。
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值