文章目录
1、三色标记算法的概述
在我们进行垃圾扫描时,学习过两种算法:引用计数和可达性分析,在Java中我们主要使用的是可达性分析。
可达性算法的三个基础算法为:标记-清除
,标记-复制
,标记-清除
。如果是在STW的情况下,那么哪种算法都不会出错,无非是效率罢了。但是随着计算器硬件的发展,与之对应的计算机软件技术及思想也在不断进步。如在多并发的基础之上,减少STW的时间,完成对垃圾的清除,边清除边标记
,这就引出了三色标记算法
。
三色指的是:黑色
、灰色
、白色
黑色
: 表示对象以及被垃圾收集器访问过,且这个对象的引用都已经扫描过。黑色的对象代表以及扫描过,他是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。灰色
: 表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描到白色
: 表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色,如在分析结束的阶段,仍然是白色的对象,即代表不可达。
2、三色标记的过程
假设现在有白、黑、灰三个集合(表示当前对象的颜色)
- 刚开始,所有对象都在
白色集合中
- 将GC Roots直接引用的对象挪到
灰色集合中
- 从
灰色集合
中获取对象:- 将本对象的引用到的对象放入
灰色集合中
- 将本对象放入
黑色集合中
- 将本对象的引用到的对象放入
- 重复步骤3,直到
灰色集合
为空结束 - 结束后,仍在
白色集合
的对象即为GC Roots不可达,可以进行回收。
注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。例如没有任何引用指向H了,它是白色,也不可能再有对象引用指向它了。
3、存在问题
用户线程和垃圾回收线程都是线程,自然就有停顿的时间,如果垃圾回收线程的时间片到了,而用户线程做了其他的操作,这就导致了会出现错标和漏标的问题。
3.1 错标
标记过不是垃圾的,变成了垃圾(也叫浮动垃圾),如图:
垃圾回收线程刚刚将D扫完,将D标为黑色
,此时垃圾回收线程时间到了,垃圾回收线程暂停
。用户线程将D指向E的关系改为,D.E=null
,从我们的角度来看,E已经是垃圾了,但是E仍然是灰色,扫描完E后,E会变为黑色,但是这并不影响这个程序的执行,在下一次垃圾回收的过程中,垃圾回收线程就会将E回收掉。
3.2 漏标
这个问题是比较严重的,如下图所示
垃圾回收线程将D标记为黑色
,表示D已经扫描完成。此时去扫描他的引用对象E
,但是此时垃圾回收线程暂停
,用户线程执行了这样一个操作:E.G = null, D.G = G
。然后垃圾回收线程回来继续扫描E
,**此时在E看来,G就是一个不可达的对象。但是D又引用着G,可是D是黑色,不会在回去重新扫一遍,垃圾回收结束,G的颜色为白色,被清除掉,D去调用G对象,出现空指针异常。**这真的是一件非常恐怖的事情。
4、解决错杀问题
上边描述过了,错杀的情况只有在 下边两个条件同时发生,才会发生:
- 黑色对象指向了白色对象
- 本来指向这个白色对象的灰色对象断开了对他的连接
4.1 CMS:写屏障+增量更新(Incremental Update)
在CMS中,如果出现这样的情况:黑色对象指向了白色对象(D.G = G
),在执行操作之前,使用钩子函数(写屏障,类似于AOP)
,将对象D从黑色变为灰色,这样就使得可以扫到D了。
那么对应灰色集合而言,就等于是一个增量更新。
4.2 G1:写屏障+原始快照(STAB)
在G1中,如此出现这样的情况:灰色对象断开了对白色对象的引用(E.G = null
),在执行操作之前,使用钩子函数(写屏障,类似于AOP)
,将这个要删除的引用记录下来,在并发扫描结束后,再扫描一遍。
这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻 的GC Roots确定后,当时的对象图就已经确定了。
比如 当时 D是引用着G的,那后续的标记也应该是按照这个时刻的对象图走(D引用着G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。