JVM在进行垃圾回收时候,首先需要确定哪些对象是需要回收的,哪些对象是不需要回收的,确认一个对象是否可以收回有以下的方案:
1.计数法
每个对象都有一个计数器,被引用了加一,移除引用减一。计数法实现简单,判断高效,但是无法解决对象之间相互循环引用的问题,这个是此算法的逻辑漏洞,因此它并不被广泛使用。
2.可达性分析法(引用链法)
以GCRoots为基础去扫描整个引用链,从而找到所有的可达对象,那剩下的其他对象就是不可达的垃圾对象了,如下所示:
图片
可达性分析法是现在被广泛使用的判断对象是否存活的算法。
1.为什么要使用三色标记算法
可达性分析法的一种实现方案是从GCRoots节点开始,使用标记-清除算法来实现。这种实现方案有两个阶段,分别是:标记阶段、清除阶段。
在标记阶段,从GCRoots节点开始扫描整个引用链,找到所有可达的对象,如下图所示的标记可达对象:
图片
在清除阶段中扫描整个引用链的不可达对象,然后将不可达的垃圾对象清除掉。这种实现方案有一个很大的缺点,那就是整个过程必须STW。CMS回收器出现之前的所有回收器,都是用这种方式实现的,因此GC停顿时间都比较长。
为了解决标记-清除算法中的问题,于是就产生了三色标记算法,目前JVM中的CMS与G1垃圾回收器所使用垃圾回收算法即为三色标记法。
2.三色标记算法的工作原理
三色标记算法是一种用于垃圾回收的标记算法,主要用于标记-清除类型的垃圾回收器。它通过将对象分为三种颜色(白色、灰色、黑色)来表示对象的状态,并通过颜色转换来判断哪些对象是可回收的。如下是几种颜色的含义:
白色:表示对象未被标记,默认情况下所有对象都是白色的。白色对象是垃圾回收的目标。
灰色:表示对象被标记过,但它的引用尚未被检查。即该对象是存活的,但它引用的对象可能仍未被标记。
黑色:表示对象已经被标记并且它引用的所有对象也都已经标记过。即该对象以及它引用的对象都被认为是存活的。
三色标记算法的执行步骤如下所示:
(1)首先创建三个集合(白、灰、黑),初始的时候将所有对象放入白色集合中。
图片
(2)从根节点开始遍历所有对象,把可达的对象从白色集合放入灰色集合,如下图所示:
图片
(3)遍历灰色集合,将灰色对象引用的可达的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合,如下图所示:
图片
(3)重复 3这个步骤,直到灰色中无任何对象,最后剩余的所有白色对象即为无用的对象,如下图所示的最终标记结果:
图片
三色标记算法通过将对象分为白色、灰色和黑色三个阶段,利用标记和清理机制来判断哪些对象是垃圾并进行回收。
3.三色标记算法的问题
以CMS垃圾收集器中使用的是三色标记算法,现在以CMS执行过程为例,如下是CMS工作图:
图片
CMS存在并发标记过程,当与用户线程一起执行的情况下标记时,由于用户线程可能会随时修改对象的引用状态,这就导致三色标记出现多标和漏标的问题。
(1)漏标的问题的产生
图片
在t1时刻,C对象被标记成灰色并且C下存在一个白色对象D;在t2时刻C不在持有D对象,但是B持有D对象,此时B由于是黑色的,那么D就不会在被标记;t3时刻由于D是白色的,那么垃圾收集就会将D清理掉。这就产生了漏标的问题。
(2)多标问题的产生
图片
在t1时刻A持有灰色对象C,在t2时刻A不再持有C对象,此时虽然C对象不可达,但是C对象被标记成灰色,那么就产生了多标问题。
4.三色标记算法中问题的解决方案
漏标问题要发生需要满足如下两个充要条件:
(1)有至少一个黑色对象在自己被标记之后指向了这个白色对象,如下图中的B对象:
图片
(2)所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用,如下所示的C对象:
图片
只有当上面两个条件都满足,三色标记算法才会发生漏标的问题。那么如果我们破坏任何一个条件,这个白色对象就不会被漏标。
CMS和G1垃圾回收器,它们都在并发标记阶段之后新增了一个重新标记阶段来校正并发标记阶段中未被正确标记的对象,CMS垃圾回收器使用的增量更新方案来解决漏标的问题,G1垃圾收集器采用的是原始快照方案来解决漏标的问题。
无论是增量更新还是原始快照都会借助写屏障来协助标记,写屏障可以理解成Spring中的AOP,写屏障可以分为写前屏障和写后屏障。
4.1 CMS解决漏标的方案
CMS回收器采用的是增量更新方案解决漏标问题,其打破的第一个条件,即有至少一个黑色对象在自己被标记之后指向了这个白色对象。
增量更新使用写后屏障,某个对象新增的引用时,将该对象记录下来,扫描完后将这个对象变为灰色对象重新扫描,在后续这个重新扫描的阶段需要用户线程STW,如下图所示:
图片
这种方式的缺点是会重新扫描新增的这部分对象,会浪费多一些时间。但是这段时间相对于并发标记整个链路的扫描来讲还是可以接受的。
4.2 G1解决漏标的方案
G1垃圾回收器采用的是原始快照的方案解决漏标问题,即破坏了“所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用”的条件。
原始快照使用写前屏障,在删除引用前保存要删除的引用,在扫描完毕后将这些删除的引用变为灰色对象重新扫描,并且GC开始后发生新增引用时,使用TAMS(Top at Mark Start)指针对新增引用进行记录。如下图所示的过程:
图片
E对象目前引用了F对象,C对象已标记完成,在下一个时刻的时候,E对象删除了对F对象的引用,D对象添加了对C对象的引用
图片
此时通过写前屏障将C对象置为灰色,F对象标记成灰色,但是F其实是浮动垃圾,等到一下时刻,从灰色队列中拉取对象重新标记,最后的结果如下所示:
图片
原始快照的缺点是会产生浮动垃圾,因为当取消对象引用的时候,有可能是真的取消引用,对象是要回收掉的。但是通过这种方式,就会把本该回收的对象又复活了,从而导致出现浮动垃圾。总体来讲,原始快照方式是可以接受的,因为在下次GC的时候垃圾还是会被回收的。
4.3、三色标记中多标的问题
在并发标记阶段,有可能之前已经被标记为存活的对象,其引用被删除,从而变成了不可达对象。
多标问题会导致内存产生浮动垃圾,但是其可以在下次GC的时候被回收,因此问题还不算很严重。
总结:
(1)三色标记算法是根可达算法的一种实现方案,其目的是为了找出所有可达对象。
(2)由于标记-清除算法效率太低,所以推出了三色标记算法,通过将对象分成白色、黑色、灰色来标记哪些对象是存活的,哪么对象需要回收。
(3)三色标记算法会产生多标和漏标问题,其中漏标问题最严重。漏标问题会导致本该存活的对象被回收,从而导致严重的程序问题。 CMS垃圾回收器采用了增量更新方式解决漏标问题,G1垃圾回收器采用了原始快照方式解决漏标问题。