垃圾回收首先要知道到底哪些对象是已经死亡、可以被回收,当前主流的编程语言的垃圾收集器基本都基于可达性分析算法来判断一个对象是否可以被GC。可达性分析算法要求全过程都基于一个能保障一致性的快照中进行,也就是必须冻结正在运行的用户线程。
那么问题来了,为什么可达性分析算法在运行时,需要一个能保障一致性的快照?如果可达性分析算法运行的线程和用户的线程并发执行,会出现什么问题?这个停顿的时间能不能减少?
想要解决这些疑问,不得不深入了解下,可达性分析算法底层到底如何运作的,它是如何标记出哪些对象是存活的。洪爵在上篇文章《面试官:如何判定一个对象是否存活?》粗略的讲到可达性分析算法大概含义,本章节来深挖它底层逻辑,当面试官一脸自信的问到这个问题,认为你答不出来的时候,请狠狠的扇他一巴掌。
首先回顾下,可达性分析算法由一系列被称之为”GC Roots“的根对象作为起始节点集,从这些根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称之为”引用链“,如果一个对象到RC Roots没有任何引用链相连,则表明这个对象不可达。
这还是比较抽象的概念,具体是如何运作我们引用三色标记法来说明:
初始状态,即还没有开始可达性分析算法,所有的对象还处于白色标记中。
开始可达性分析算法后,把GC Roots直接关联到的对象加入到【灰色标记队列】中。
遍历对象A、C、K的属性中引用的对象,加进【灰色标记队列中】。
这个时候A、C、K对象的属性中直接引用的对象都访问完了,A、C、K三个对象从【灰色标记队列】中移除,加入【黑色标记队列】中,表示该对象是可达,即存活状态。
按照同样的动作,访问B、D属性中直接引用的对象,加入到【灰色标记队列】中。
标记B、D对象为黑色,加入到【黑色标记队列】。
以此类推,接下来分别是:
最后A~I对象都被标记为黑色,而L、M、N三个对象被标记为白色(没有标记白色这个动作,只是为了方便理解),代表没有访问过,可以被回收。
有的同学就会问了,**如果L、M、N对象中的任何一个后面又被GC Roots直接或者间接关联了呢?被回收岂不是不合理?**好的,问题非常好,这个是不会出现的情况,因为已经这三个对象已经丢失了,不知道它们的内存地址的位置。
我们已经了解了可达性分析算法的具体运作过程,那么我们来分析一下,**为什么可达性分析算法在运行时,需要一个能保障一致性的快照?**举一个可达性分析算法和用户线程并发运行的例子,同样也继续使用三色标记法来为大家说明:
A、C、K三个对象已经被标记为黑色,即已经访问过,并且属性中引用的对象也访问了,即B和D对象,已经被标记为灰色,表示已经访问但B和D对象中属性引用的对象还有没有访问。这个时候用户并发线程做了一个操作,K对象中某个属性指向了对象G,然后断开了对象D引用对象G的关系。
而对象K已经被标记为黑色,代表该对象被访问过,并且属性中的引用对象也被访问过。所以对象G直到最后可达性分析算法结束,都不会被标记为灰色或者黑色。这就会出现回收了存活对象的问题,直接影响了程序的正确性。
可达性分析需要一个能保障一致性快照的场景下运行的原因已经找到了,那么我们有没有什么可以优化的点,让STW的时间缩短或者让可达性分析线程可以和用户线程并发运行呢?
Wilson在1994年理论上证明,当且仅当同时满足以下两个条件时,才会产生“对象”消失的问题:
那要解决并发过程中因为引用关系导致的存活对象被回收问题,我们可以打破其中任何一个条件即可,打破第一条规则我们称之为增量更新、打破第二条规则我们称之为原始快照。
增量更新是啥意思呢,当黑色对象新增一个对白色对象的引用关系时,我们把这个引用关系给记录下来,等并发标记完对象后,再将记录过的新的引用关系中黑色对象重新走一遍标记流程,可以简单的理解为该黑色对象重新被标记为了灰色对象。
原始快照当灰色对象要删除对白色对象的引用,那么就将要删除的引用记录下来,在并发扫描结束后,会按照刚开始扫描那一刻的对象图来进行标记。
很多脑洞大开的童鞋就会想到,如果在增量更新后,又出现黑色对象对白色对象的新引用,那岂不是俄罗斯套娃,永无止境?其实有很多办法可以解决,比如说对重新标记的场景进行STW等等。
到了文章末尾,洪爵又有了新的问题提出给大家**,增量更新和原始快照是如何实现的,如何在并发的情况下进行记录?**别着急,洪爵下一篇文章告诉你(禁止套娃)。
道阻且长,往事作序,来日为章。
期待我们下一次相遇!