标记清除算法
在三色标记之前有一个算法叫标记清除算法。这个算法会设置一个标志位来记录对象是否被使用。最开始所有对象的标记都是0,通过根可达分析算法发现对象存活,就会置为1。一步步执行下去就会呈现出一个类似树状的结构。等标记的步骤完成之后,会将未被标记的对象统一清理,再次把所有的未被回收的标记重置为0,反复执行。
这个算法最大的问题是,GC在执行期间需要把整个程序完全暂停,不能异步进行GC操作。因为在不同阶段标记清除算法的标志位0,1具有不同的含义,如果并发处理,则有可能会额外删除新增的存活对象(所有对象一开始都是0)。
对实时性要求高的系统来说,这种需要长时间挂起的标记清除时不可以被接受的。此时就需要一个算法来避免这种STW的现象,这个算法就是三色标记法。
三色标记算法
三色标记最大的好处就是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行GC环节。
三色标记,见字知意。使用三种颜色标记对象的一种算法。
*黑色:*根对象,以及该对象与它的子对象都被扫描过。
*灰色*:对象本身正在被扫描,其子对象还未被扫描。
*白色*:未被扫描的对象。如果扫描完所有对象后仍为白色,则被判定根不可达,即为垃圾。
漏标问题
三色标记的过程中,标记线程和用户线程是并发执行的,那么就有可能在我们标记过程中,用户线程修改了引用关系,把原本应该回收的对象错误标记成了存活。(简单来说就是GC已经标黑的对象,在并发过程中用户线程引用链断掉,导致实际应该是垃圾的白色对象但却依旧是黑的,也就是浮动垃圾)。这时产生的垃圾怎么办呢?答案是不怎么办,留给下次垃圾回收处理。
而漏标问题,意思就是把本来应该存活的垃圾,标记为了死亡。这就会导致非常严重的错误。那么这类垃圾是怎么产生的呢?
途中对象A被标记为了黑色,此时它所引用的两个对象B,C都在被标记的灰色阶段。此时用户线程把B->D之间的的引用关系删除,并且在A->D之间建立引用。此时B对象依然未扫描结束,而A对象又已经被扫描过了,不会继续接着往下扫描了。因此D对象就会被当做垃圾回收掉。
总结:漏标问题的产生有以下两个必要的条件:
1:至少有一个黑色对象在自己被标记之后指向了白色对象。
2:删除了灰色对象到白色对象的直接或间接引用。
新增对象算漏标问题么?
答案是:不算。
不知大家在学习三色标记的时候是否有过这种疑惑。为什么漏标的对象必须要删除灰色对象到白色对象的直接或间接引用。那这种直接新增的对象为什么不会被三色标记判定为漏标呢?
TAMS
要达到与GC用户并发运行,必要要解决回收过程中新对象的分配(否则在一条GCRoots标记完成之后,新来的对象会被当做垃圾回收),所以G1在每个Region中设置了两个名为TAMS的指针,在Region划分出部分空间用于记录并发回收过程中的新对象,认为他们是存活的,不纳入垃圾回收。因此,我们在分析漏标问题时,便可以严格遵守漏标问题产生的两个条件了。
解决漏标问题
所以我们主需要打破其中任意一个条件就可以处理漏标问题,对于以上问题,CMS与G1各采用了一套方案进行处理。
CMS---增量更新
增量更新打破了第一个条件。当A插入新的引用关系D时,就将这个插入的引用记录的A对象记录下来。等待扫描结束后的重新标记阶段,再把这些记录过的引用了新对象的对象(如上图A)再次变为灰色。就会重新对A再进行一次扫描。
由图可见,此方法再次执行可以处理漏标的D对象。*但是B,C对象属于被重复扫描的对象。*
G1---SATB
在学习JVM的过程中,相信大家听到增量更新都觉得很好理解。然而SATB却很难理解。(反正博主是N刷JVM后才有了一些体会)。
我们知道STAB是破坏了条件2:删除了灰色对象到白色对象的直接或间接引用。
情况1:打破灰色引用的直接引用B->D
情况2:打破灰色引用的间接引用D->E.
无论是上述任何一种情况,只要是属于B对象引用链上的白色对象变为了垃圾。STAB此时被称为快照的原因在于此时它记录了引用链断开前的快照,并且记录对象B。
当到了最终标记阶段,GC线程会把B对象变为GCRoots,无论之后的对象是否存活,一律按照快照中的存活状态全部进行扫描。这样整个引用链都会保留下来。
我们不难分析出SATB采取的属于保守原则。若是在并发标记过程中发现灰色对象之后的对象变为垃圾*(因为一旦有对象变为垃圾,就有可能被其他确认存活的黑色对象拯救)。*所以就整个引用链的所有对象判定为存活。
而此处以灰色对象为GCRoots重新扫描,可以最大程度的较少以原本GCRoots重新扫描的成本。可以看到SATB的处理策略有些像浮动垃圾的处理策略。
总结
CMS关注新增,但是会把新增对象的前一个引用变为灰色,重新对所有子对象进行遍历,有一定性能损耗。G1引用的打破,不需要重新遍历对象,因此效率较高。但是维护快照,以及其保守策略增加了一定的内存成本。