垃圾回收算法(6)三色标记
GC目前的问题是,会暂停、阻碍代码的运行,即stop the world。增量式GC处理的就是这个问题。将GC变得可一阶段一阶段进行。
分阶段运行的思路并不难,但具体要解决的问题其实是分阶段GC后,如何保证下次继续时,中断过程中引用关系的变化不会对GC造成影响。
三色标记法是一个逻辑上的抽象,将对象分成白:未搜索,灰:正搜索,黑:已搜索。
在这里,和前面引用计数中提到的标色不一样,这里只是一个逻辑概念,在实现中并没有所谓的black, white。
mark_sweep按增量来排,可以分成三个阶段:根查找、标记、清除
incremental_gc() { case $gc_phase if GC_ROOT_SCAN root_scan_phase() if GC_MARK incremental_mark_phase() else incremental_sweep_phase () } root_scan_phase() { for r : $root mark(r) $gc_phase = GC_MARK } mark(obj) { if !obj.mark obj.mark = true push(obj, $mark_stack) // 理解下,不分段的GC中,由于是用递归方式直接深度搜索到底,所以不需要这个stack,而这个搜索过程目前会中断了,因此需要这样一个数据结构来记录。 }
上面这mark,就逻辑上把根对象由白标记为灰了。
incremental_mark_phase() { for i : range 1..MARK_MAX // 有个值,每次就处理这么多,可以有效防止stop the world if !is_empty($mark_stack) // 以下栈中有值就取,无值就扫root obj = pop($mark_stack) for child : children(obj) mark(child) else for r : $root mark(r) while !is_empty($mark_stack) obj = pop($mark_stack) for child : children(obj) mark(child) $gc_phase = GC_SWEEP // 直接进入下阶段 $sweeping = $head_start return } // 清除就不说了,同样思路,设置个最大值,每次只处理这么多。因为是mark_sweep,所以只要将未标记的引入free_list即可!!!!
到这里遇到了关键问题:如果在垃圾回收阶段中间有新的对象引入,或是由于对象的指向关系,使得原本应该mark到的活动对象漏掉了,怎么办?这里会出现因为此对象没有mark而被清除的问题。
新对象加入好说,对象的指向变化导致没有mark到,是这种情况:
上图,C原先是应该被B递归搜索标记的。但在GC休息时,B不再指向C,C反而被A指向了。这个C在本轮就会被回收掉。
这个问题是三色与mark之间的对应关系没有对应好导致。
现在入mark_stack栈且mark与灰对应,搜索完成后,mark的是黑。而垃圾回收的依据,是mark过的对象,黑。而白,一定是非mark过,一定会被回收,但这里,白不应该被回收。因此,这个C对象的白色是错误的,要处理。
wirte_barrier(obj, field, newobj) { if newobj.mark == FALSE newobj.mark = true // 这里,因为本身write_barrier是一个赋值操作,因此此对象天生就被mark也算正常 push(newobj, $mark_stack) // 这个动作,就强行标记为灰了 field = newobj }
处理后,新引用的对象也是mark状态,是这样的:
最后,如果新分配对象时,mark阶段已经完了,正在sweep,怎么处理?很简单,只要判断分配的对象在sweeping指针的前面还是后面。如果在前面已经sweep过的区域,直接忽略;如果在后面,简单mark下就可以。
优点:
- 不会长时间停
缺点:
- write_barrier略有开销
- 上面write_barrier会将对象强行制灰,也就是强行标记,是不大精确的,会造成当前轮次的垃圾残留。
针对缺点2:
场景是,write_barrier后,是对的。但再次回头,比如A又指向B了,那C这个垃圾在本轮就发现不了。
改良型(steele)的write_barrier
mark(obj) { if !obj.mark push(obj, $mark_stack) // 和上面对表,少了mark = true } // 上面减少了mark的工作,将mark稳定到出栈处。这样可以引出下面的write_barrier // 这里,灰色已经不再是mark过,而是入过栈。反而,黑色才是mark过。 write_barrier(obj, filed, newobj) { if $gc_phase == GC_MARK && obj.mark && !newobj.mark // 逻辑也很清晰,不再一棍子将新加入的认为是非垃圾,而是认为“需要check是否垃圾”。如何check,就是将引用它的对象回滚成灰。 obj.mark = false push(obj, $mark_stack) field = newobj }
即:
还有基于快照思想的一种write_barrier的思路:
在write_barrier中入mark_stack栈的不是新对象,而是旧对象!这样,对于之前的对象的引用仍然存在,就不会丢对象。那么mark阶段中新生成的对象怎么处理?它直接将其mark,过于保守。