前情提要,当前主流编程语言的垃圾收集器,基本上都是依靠 可达性分析算法 来判定对象是否存活。然而,可达性分析算法要求全过程都基于一个能保障一致性的快照中才能够进行分析,简单来说,就是必须全程冻结用户线程的运行,这样很影响运行效率。
为了减少用户线程冻结的影响,提高运行效率,在这里我们引入一个概念:并发标记。接下来,让我们一起去了解并发标记是什么。
并发标记的作用
首先提出一个问题,为什么全程冻结用户线程的运行,很影响运行效率?
根据 可达性分析算法 的核心概念,利用系列根对象(GC Roots )作为起始点,根据对象之间的引用关系搜索出一条引用链(Reference Chain),通过遍历引用链来判断对象的是否存活。
在这个过程中,根对象枚举的时间非常短暂且相对固定,然而,遍历所有引用链(对象图)所需要的时间与对象的数量成正比。因此,对象越多,对象之间的引用关系就越复杂,需要更多的时间去遍历所有的引用链来 标记 所有的对象。
并发标记的作用就是让垃圾回收线程和用户线程能够同时进行,并发执行。
并发标记的问题
那么,垃圾回收线程和用户线程并发执行的过程中会遇到什么问题?
为了更好的解释问题,需要引入 三色标记(Tri-color Marking)进行辅助说明,在遍历所有的引用链(对象图)的过程中,按照 “是否访问过” 这个条件,将对象标记成以下三种颜色:
◉ 白色:
(1)表示对象尚未被垃圾收集器访问过。
(2)可达性分析开始前,所有对象都是白色的,如果分析结束后,依然是白色的对象,意味着它是不可达的,将会被回收。
◉ 黑色:
(1)表示对象已经被垃圾收集器访问过,而且对象的引用链已经遍历完成。(2)黑色的对象,意味着它是可达的,不会被回收。
(3)如果被其他对象引用,不需要重新遍历一遍。
(4)黑色的对象不可能直接(不经过灰色的对象)指向某个白色的对象。
◉ 灰色:
(1)表示对象已经被垃圾回收器访问过,但是对象的引用链没有遍历完成。
(2)灰色的对象在黑色的对象和白色的对象之间。
![图片](https://i-blog.csdnimg.cn/blog_migrate/810b6129919a72b56f09024212c4f09b.png)
然而,当垃圾回收线程和用户线程并发执行,有可能会出现两个问题:浮动垃圾 和 对象消失。
什么是浮动垃圾
浮动垃圾,简单来说就是将对象错误的标记为存活,虽然这样会影响垃圾回收的整体效率,但是并没有影响程序的正常运行,下次再进行垃圾回收清理即可,无伤大雅。
那什么情况下会出现浮动垃圾的问题?接下来用图文进行说明。
(1)假设对象图的初始状态,如图2所示:
![图2](https://i-blog.csdnimg.cn/blog_migrate/aa89b84536d460fce733a23b890bd149.png)
(2)当遍历标记到 对象D 的时候,如图3所示:
![图3](https://i-blog.csdnimg.cn/blog_migrate/d6486532c86d08dc66bb5eb4b24c2e2e.png)
(3)此时,如果用户线程取消了 对象B 指向 对象K 的引用,如图4所示:
![图4](https://i-blog.csdnimg.cn/blog_migrate/5802e4ebbfc2ef879c1fd00c66ae3d63.png)
(4)最终,对象图的标记情况如图5所示,然而,对象K 已经被标记为黑色的对象,对象K 将不会被垃圾回收器回收,对象K 就是浮动垃圾。
![图5](https://i-blog.csdnimg.cn/blog_migrate/57155f490e9300c31c5ba922a8176b3e.png)
(5)实际上,正确的对象图最终的标记情况如图6所示。
![图6](https://i-blog.csdnimg.cn/blog_migrate/559b922f32fd3777c78f35b29b0702b4.png)
什么是对象消失
对象消失,简单来说就是把原本存活的对象错误的标记为垃圾,这是致命的错误,将会导致程序出现错误。
那么什么情况下会出现对象消失的问题?接下来用图文进行说明。
(1)假设对象图的初始状态,如图7所示:
![图7](https://i-blog.csdnimg.cn/blog_migrate/aa89b84536d460fce733a23b890bd149.png)
(2)当遍历标记到 对象B 的时候,如图8所示:
![图8](https://i-blog.csdnimg.cn/blog_migrate/082f419e6ebed00c6d4fb22fc62446e9.png)
(3)此时,如果用户线程取消了 对象B 指向 对象K 的引用,并且添加了 对象A 指向 对象K 的引用。如图9所示。
![图9](https://i-blog.csdnimg.cn/blog_migrate/21cc3879cdcef67087ee319a6c4f3e94.png)
(4)由于 对象A 已经标记为黑色的对象,因为不会再次遍历 对象A 的引用链,这将会导致 对象K 没有被垃圾收集器访问。最终,对象图的标记情况如图10所示。
![图10](https://i-blog.csdnimg.cn/blog_migrate/9f6375f20e35881bf31061ff0793906d.png)
(5)实际上,正确的对象图最终的标记情况如图11所示。
![图11](https://i-blog.csdnimg.cn/blog_migrate/977a25c954a797748afa984dfee334c6.png)
如何解决对象消失的问题
分析上面的对象消失的图文过程,当且仅当满足以下两个条件是,才会发生对象消失的问题,即 黑色对象 被错误标记为 白色对象,如图9所示。
1、用户线程添加了一个或多个 黑色对象 指向 白色对象 的引用。
2、用户线程取消了所有 灰色对象 指向 白色对象 的引用。
![图9](https://i-blog.csdnimg.cn/blog_migrate/f489b0ad27593d7e92609b020ab300e7.png)
针对这两个条件,分别产生了两种解决方案:增量更新 和 原始快照。
增量更新 就是破坏第一个条件,当添加 黑色对象 指向 白色对象 的引用时(A -> K),将这个新增的引用记录下来,等并发扫描结束之后,再将这些引用记录中的 黑色对象 作为根节点,重新遍历一次这个 黑色对象 最新状态的引用链。
可以简化理解为,黑色对象 一旦添加了指向 白色对象 的引用,就会变成 灰色对象。
原始快照 就是破坏第二个条件,当取消 灰色对象 指向 白色对象 的引用时(B -> K),将这个删除的引用记录下来,等并发扫描结束之后,再将这些引用记录中的 灰色对象 作为根节点,重新遍历一次这个 灰色对象 原始状态的引用链。
可以简化理解为,这个删除操作被忽略,删除的引用链依旧会被遍历。然而,这可能会导致另一个问题,造成更多的浮动垃圾。
因为删除的引用记录遍历之后,灰色对象(B)引用的 白色对象(K)将会变成灰色对象。如果实际情况中白色对象(K)并没有被任何对象引用,那么白色对象(K),乃至它引用链上的对象都是浮动垃圾。
再说几句
并发的场景下,可达性分析算法标记的过程中,利用三色标记法标记对象,会发生浮动垃圾和对象消失的问题,针对第二个问题,由此引出了增量更新和原始快照。
以上的引用关系记录的插入和删除,在虚拟机中都是通过读写屏障实现的,在 Java HotSpot 虚拟机中,增量更新和原始快照都有实际应用,例如,CMS 是基于增量更新来做并发标记的,G1、Shenandoah 则是用原始快照来实现。