垃圾回收算法(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下就可以。
 
优点:
  1. 不会长时间停
 
缺点:
  1. write_barrier略有开销
  2. 上面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,过于保守。
 
 
 
 
原文地址:https://www.cnblogs.com/qqmomery/p/6661574.html