三色标记
在遍历对象图的过程中,把需要遍历的对象按照“是否访问过”分为以下三种颜色。
- 白色:表示对象尚未被垃圾回收器访问过。显然,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过。
上图是在可达性分析的扫描过程中,如果只有垃圾回收线程在工作,那肯定不会有任何问题。
如果用户线程和GC线程并发执行时,就会出现对象消失的情况。
并发标记对象漏标
如上图所示,老年代中有三个对象A、B和C,在标记之前的引用关系是A引用B,B引用C。先分别介绍一下他们的标记情况:
- 对象A已经被标记为黑色,表示为A为活跃对象且所有它引用的对象也完成标记。
- 对象B被标记为灰色,表示B对象是活跃对象,但是它关联的对象还没有被完全标记完。
- 对象C是白色,表示还没有被标记。
在这种背景下,应用线程此时发生了一次引用关系变更,B引用C的关系被删除了且同时A引用了C,即如下所示代码:
B.C = null; A.C = C;
此时,标记线程就不再会标记对象C,因为对象A已经是黑色,表示所有它引用的对象都已经完成标记。然而实际上活跃的对象C就会被漏标最终被回收掉。
如何解决漏标问题?
一个叫Wilson的大佬,他在1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生"对象消失"的问题,原来应该是黑色的对象被误标为了白色:
- 条件一:赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
- 条件二:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
反过来想,是不是破坏其中一个条件,就不会存在对象消失的情况了吧。
因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB) 。
- 增量更新(Incremental Update):打破了第一个条件,在扫描过程中,如果黑色对象插入了新的白色对象引用,那么就将这些黑色对象记录起来作为GC Roots,在Remark阶段重新扫描以这些为Root的引用链。其实就可以很简单的理解成,在黑色对象插入新的白色对象引用以后,就重新变成了灰色对象。
- 原始快照(Snapshot At The Beginning,SATB):打破了第二个条件。在扫描过程中,如果出现灰色对象删除白色对象的引用时,就将这些灰色对象记录下来。在Remark阶段,对这些灰色对象重新扫描一遍。
以上无论是对引用关系记录的插入还是删除通过“写屏障”来完成。
- 增量更新使用的是写后屏障(记录插入后的新引用)
- 原始快照使用的是写前屏障(记录删除后的前引用)
在HotSpot虚拟机中, 增量更新和原始快照这两种解决方案都有实际应用, 譬如, CMS是基于增量更新来做并发标记的, G1则是用原始快照来实现。