谈到JVM我们肯定会谈到垃圾回收,而垃圾回收最前置的知识点一定是如何判断对象是否死亡。 引用计数法和可达性分析算法大家肯定已经耳熟能详了,今天深入讲解下可达性算法中的那些事儿。
如何判断对象是否死亡
对堆垃圾回收前的第一步就是要判断哪些对象已经死亡
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个算法很难解决对象之间相互循环引用的问题。
可达性分析算法
通过一系列的称为 GC Roots 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象
三色标记
可达性分析算法中,有一个过程是需要遍历对象图的。
在遍历对象图的过程中,把访问都的对象按照是否访问过这个条件标记成以下三种颜色
白色,表示对象尚未被垃圾回收器访问过。在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象代表不可达。
黑色,表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接指向某个白色对象。
灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过。
灰色对象是黑色对象与白色对象之间的中间态。
当标记过程结束后,只会有黑色和白色的对象,而白色的对象就是需要被回收的对象。
哪些对象可以作为 GC Roots 呢
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
对象可以被回收,就代表一定会被回收吗
即使在可达性分析法中不可达的对象,也并非是非死不可的,这时候它们暂时处于缓刑阶段,要真正宣告一个对象死亡,至少要经历两次标记过程;
可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。
当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
可达性分析的并发标记用途是什么
可达性分析算法需要基于能保障一致性的快照中才能够分析,这意味着必须全程冻结用户线程的运行。
可达性分析算法中,根节点的枚举阶段是不太耗时的,但是标记过程的耗时是会随着java堆里面存储的对象增加而增加的。
并发标记就是要消减这一部分的停顿时间。让垃圾回收器和用户线程同时运行,并发工作。
并发标记带来了什么问题
浮动垃圾和对象消失。
可达性分析算法中,存在三色标记过程。
垃圾回收器在对象图上面标记颜色,而同时用户线程在修改引用关系,引用关系修改了,那么对象图就变化了
有可能把原本消亡的对象错误标记为存活,这就会产生逃过垃圾回收的浮动垃圾。
也有可能把原本存活的对象错误的标记为已消亡,还需要使用的对象被回收了,程序就会发生错误。
并发标记的对象消失问题图示
1、初始状态,只有GC Roots是黑色的。
2、扫描的过程变化
3、当扫描顺利完成后,对象图就变成了这个样子
黑色对象是存活的对象,白色对象是消亡了
消失的情况一:用户线程在标记的时候,修改了引用关系
扫描完成后,对象图就变成了这个样子
原本还在被对象5引用的对象9,由于是白色对象,所以根据三色标记原则,对象9会被当成垃圾回收,这样就出现了对象消失的情况。
消失的情况二:用户线程切断引用后重新被黑色对象引用的对象就是原来引用链的一部分
当扫描完成后,对象图就变成了这个样子
黑色对象不会重新扫描,这将导致扫描结束后对象10和对象11都会回收了。他们都是被修改之前的原来的引用链的一部分。
如何解决对象消失
当且仅当以下两个条件同时满足时,会产生对象消失的问题,原来应该是黑色的对象被误标为了白色:
1、插入了一条或者多条从黑色对象到白色对象的新引用。
2、删除了全部从灰色对象到该白色对象的直接或间接引用。
黑色对象5到白色对象9之间的引用是新建的,对应条件一。
黑色对象6到白色对象9之间的引用被删除了,对应条件二。
要解决并发标记时对象消失的问题,只需要破坏两个条件中的任意一个就行。
于是产生了两种解决方案:
- 增量更新
增量更新要破坏的是第一个条件。
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
可以简化的理解为:黑色对象一旦插入了指向白色对象的引用之后,它就变回了灰色对象。
- 原始快照
原始快照要破坏的是第二个条件。
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这个可以简化理解为:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照开进行搜索。
无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。
写屏障可以看作虚拟机层面对引用类型字段赋值这个动作的AOP切面,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫做写前屏障,在赋值后的则叫作写后屏障。
增量更新用的是写后屏障,记录了所有新增的引用关系。
原始快照用的是写前屏障,将所有即将被删除的引用关系的旧引用记录下来。
参考资料
1、JavaGuide:javaguide.cn/
2、你说你熟悉jvm?那你讲一下并发的可达性分析:juejin.cn/post/684490…