并发可达性分析遇到的问题
前面说完了可达性分析。基本对于垃圾回收如何判断对象是否存活便有了一个大概的认识。下面,我们补充一个知识点,并发可达性分析,也是为后面讲垃圾收集器做铺垫。
在JVM进行可达性分析时,一般其他的java用户线程是没有停止的,它们还在辛勤的劳动。那么此时如果用户线程改变了引用关系。
比如在上图的基础上,obj3又引用了一个obj4,此时怎么办呢?又或者obj3与obj1的引用关系断了,obj3死亡了,此时该怎么办呢?所以接下来,我们就讲讲并发的可达性分析。(所谓并发,在于用户线程和垃圾回收线程同时运行,如果同时只有一种线程运行,那就不存在并发问题了)
首先,我们明确一点,在进行可达性分析时,首先JVM会将所有可以作为GC Roots的对象设置成GC Roots,这一过程是需要STW的(stop the world,该过程要冻结用户线程的运行,不过这一过程耗时很短,几乎可以忽略不计)。之后便是要开始往下扫描了。
我们引入三色标记:
- 白色,表示对象尚未被垃圾收集器访问过
- 黑色,表示对象已经访问过,并且其所有引用的对象也访问过(即该节点被访问过,该节点的所有子节点也被访问过)
- 灰色,表示该对象被访问过,但是其所有引用的对象都没有被访问过(即该节点被访问过,该节点的所有子节点却没有被访问过)
不过因为是并发可达性分析,在确定了GC Roots之后,我们继续往下扫描,此时用户线程可能会对可达性分析造成干扰(在本节一开始我们也提到了这些),详情见图(在word里画的,有点丑,将就看哈哈)。
这里JVM从0这个GC Roots开始扫描,当扫描了2时,正准备扫描其子节点3时,此时用户线程对引用关系进行了修改,将2-3的引用关系删除,添加了1-3的引用关系,如下图所示(虚线是删除)。
可以看到此时出问题了,即使是修改了之后,从我们用户的眼光来看,3依然是可达的,但是在jvm角度来看,1是黑的,它和它的子节点不会再被扫描,所以扫描不到3,2是灰的,但是它与3之间没有引用关系。此时,jvm就会错误的认为3是不可达的。
原因在于
出现这个问题的原因在于二个条件:
- 一是添加了一个新的引用
- 二是删除了旧的引用
单独的出现一个条件是无所谓的,比如我们仅仅删除一个旧的引用,此时只是该对象死亡了而已,并没有影响,如图:
如果仅仅是添加也是不影响的,如图所示
所以问题出现在,删除和添加同时进行,那就出现错误了。所以这里我们需要破坏其中任意一个条件即可。
解决方法
对于破坏添加新引用这个条件,我们称之为增量更新。当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等我们的扫描(遍历)结束之后,在以我们记录中的黑色对象为GC Roots,再扫描一遍。这里需要注意,再次扫描的不是整个树哦,而是那些有过添加引用操作的黑色对象。所以第二次扫描的时间会很短。
我们来跟着例子实际走一遍:
如上图,还是跟之前一样,不仅删除了2-3的旧引用,还添加了1-3的新引用,这要是在之前,3肯定是访问不到了。但当我们启用了增量更新之后,情况就不一样了。与上面一样,当用户删除了2-3引用关系之后,无操作,但增加了1-3引用之后,JVM记录下1-3这个新增加的关系,当这一轮扫描完成之后,3确实没被扫描到。不慌,我们进行第二次扫描,从记录中看到1-3这个关系,便以1为根节点开始扫描,那么此时,便可以扫描到3,保证了正确的结果。
而对于破坏删除旧引用这个条件,我们称之为原始快照。其做法是:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,等本次扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,再重新扫描一次。
同样,我们拿上面的例子走一遍,再放一次图
在引入了原始快照之后,当删除了2-3引用关系时,我们记录下这一引用关系,而添加了1-3引用关系,这不管,随他去,本次扫描结束后,3不可达。然后进行第二次扫描,我们从记录中发现之前删除了2-3引用关系,此时便在记录中扫描,记录中明显有2-3,则从记录中的2,肯定能扫描到记录中的3,那么3便可达。
这里是不是有些迷糊?对的,如果按照原始快照在书上的阐述(感觉书上阐述的不清楚),重新再扫描一次,肯定是扫描不到3的,因为引用关系已经变了,所以重新扫描是扫描的原来的引用关系。也就是我们记录中已经删除的引用关系,因为只有这个才是原始的。
所以这也是原始快照这个名字的由来,(所以它没有起一个什么删除不变的土里土气的名字),无论引用关系删除与否,我都按照我第一眼看到的那样来扫描,那么就不可能出现有对象还活着,但我误以为它死了的误判了
但这会导致一个问题,就是下图这种情况,
就是加入只删除,不添加,那么3不是确实不可达了嘛,但根据原始快照,3又可达,这不是矛盾吗?确实矛盾,但是不影响,因为你想,活的对象你把它判定死了,这是要出事的,但是死的对象你判定它活着,这顶多多耗你一丢丢内存呗,基本都没有本质上的区别,所以,这不是个问题。
在CMS中,用到了增量更新,而G1中用到了原始快照,这个我们之后还会讲这些。
参考资料
- 《深入理解JVM》周志明