zoukankan      html  css  js  c++  java
  • 垃圾回收算法(2)引用计数

    引用计数:
     
    引用计数于1960年被提出。思想是在对象中增加一个“被多少个外部对象引用”的字段。当没有外部引用时,字段自然为0,说明是垃圾了。
     
    对象的分配延续前文,以free_list管理。
     
    它与上文的mark_sweep区别在于,gc并非显式调用,而是伴随着对象的分配与覆盖(pa = pb,即pa原值被覆盖)发生。内存管理与应用流程同步进行是引用计数的特征。
     
     
    这样,new_obj的流程为:
    new_obj(size) {
      obj = pickup_chunk(size, $free_list)
      if obj == NULL
        fail;
      else 
        obj.ref_cnt = 1 // 初始化为1
        return obj
    }
     
    同样,指针的“覆盖”如下:
    update_ptr(ptr, obj) {
      inc_ref_cnt(obj)
      dec_ref_cnt(obj) //这两者不可调换,因为可能ptr, obj是同一对象
      *ptr = obj
    }
     
    inc_ref_cnt(obj) {
      obj.ref_cnt++
    }
     
    def_ref_cnt(obj) {
      obj.ref_cnt--
      if (obj.ref_cnt == 0) 
        for (child : children(obj))  // 当没人引用时,对所有子对象进行递归
          def_ref_cnt(child)
      reclaim(obj)   // 塞入free_list
    }
     
    引用计数的经典逻辑就是这么简单,看一下它的优缺点:
    优点:
    1. 内存完全不会被垃圾占用,一有垃圾可以立即加收;
    2. 没有一个“最大暂停时间”;
     
    当然缺点也很明显:
    1. 每次更新指针(覆盖)都会伴随引用计数的流程,计算量比较大;
    2. 计数器本身需要占位,如果每个对象占内存空间,内存空间与最大被引用数相关;
    3. 实现烦琐。算法虽然简单,但修改代码将pa=pb换为update_ptr时容易遗漏导致问题;
    4. 循环引用无法处理。
     
    下面针对这些缺点看一下对应的一些方法。
    1. 计算量大
    可以缩减计算范围,比如,从根(如mark sweep中描述的root)出发的全局变量的指针覆盖,并不用update_ptr变更计数,那这样会有一些对象引用计数为0但仍被root引用着,可以使用一个zero count table来记录这些对象。这样可以大大减少因引用计数为0时的计算量。而本身因引用计数降为0应该被回收的垃圾,则在专门的逻辑中处理,到时再放入free_list中。
     
    dec_ref_cnt(obj) {
      obj.ref_cnt--
      // 引用计数为0时,并不会立即回收内存,而是放入zct中。只有zct满了,才会。
      if obj.ref_cnt == 0 {
        if (if_full($zct)) {
          scan($zct)
        }
        push($zct, obj)
      }
    }
     
    scan_zct() {
      for (r: $root) 
        r.ref_cnt++
     
      for (obj: $zct)
        if obj.ref_cnt == 0 
          remove($zct, obj)
          delete(obj)
     
      // 很简单,只是先加后减,操作后zct中的引用计数仍为0,且被root引用着
      for (r : $root) 
        r.ref_cnt--
    }
     
    // 最后看下delete,很简单,是真实的回收。
    delete(obj) {
      for (child : children(obj))
        child.ref_cnt--
        if child.ref_cnt == 0 
          delete child
      reclaim(obj)
    }

    以上则是引入zct后,减少引用计数时的逻辑。

     
    同样,在new时,如果内存不足,则调用一次scan_zct,再重新分配一遍即可。
    这个方法会增大最长暂停时间。
    1. 计数器本身的内存占用:
    这个问题的含义是,如果因为计数器内存占用考虑而设得太小,比如5位,那么只能记录被32个对象引用,超过后计数器就溢出了。
    “sticky"引用计数法是处理是处理这种问题的思路。研究表明,很多对象一生成立即就会死了,也就是说大多数对象的计数是在0,1之间,达到32的本身很少。另一方面,如果真有达到32个引用的对象,那么很大程度上这个对象在执行的程序中占有重要的位置,甚至可以不需要回收!
      另一个方法是适当时候启动mark-sweep来进行一轮清理,这时mark不需要额外使用标志位,直接使用引用计数就可以。这样不仅可以将溢出的引用计数回收,也可以将循环引用的垃圾回收
      此外,还可以引出一个极端的方法,1位计数法。这种方法将引用计数从对象中剥离,而放在引用对象的指针中(由于字节对齐,指针的最后几位用不到)。这样不仅有上述sticky引用计数的优点,而且可以带来更高的缓存命中率,因为对象引用关系变化时,对象本身的内存是不变的。
     
    1. 循环引用问题
    第2个问题的解决方法中提到了,解决循环引用的一种方式是某个时机加入mark-sweep算法。但事实上这是个很低效的办法。因为引入这种全堆的扫描仅仅是为了极少量存在的循环引用,显然不合适。
    因此,可以引入优化,将扫描范围由全堆缩减到“疑似循环引用对象的集合”,这就是部分标记-清除算法(partial mark sweep)
     
    它的核心思想是,找出一个可能是循环引用垃圾(注意,不是找循环引用,是找循环引用垃圾)环中的一个对象,将其放置入一个特殊的集合。对这个集合进行mark-sweep,判断出是否真的循环引用了。
    算法如下:
     
    将对象分为4种:
    black:确定的活动对象;white:确定的非活动对象;hatch:可能是循环引用的对象;gray:用于判断循环引用的一个中间态。
     
    算法的切入点在于减引用计数,如下:
     
    def_ref_cnt(obj) {
      obj.ref_cnt--
      if obj.ref_cnt == 0  // 引用为0,绝不可能是循环引用垃圾
        delete(obj)            // delete函数上面有,减子对象的引用计数并回收
      else if obj.color != HATCH   // 可见,疑似循环引用垃圾的必要条件,是被减引用后,计数未达0
        obj.color = HATCH
        enqueue(obj, $hatch_queue)  // 这里仅仅将可疑的对象本身入队列
    }
    对应的,new:
    new_obj() {
      obj = pickup_chunk(size)
      if obj != NULL 
        obj.color = BLACK
        obj.ref_cnt = 1
        return obj
      else if !is_empty($hatch_queue)
        scan_hatch_queue()      // 当无内存可用时,开始检测循环引用队列并释放之
        return new_obj(size)
      else
        fail()
    }
    下面,便是如何判断循环引用垃圾的核心逻辑:
     
    scan_hatch_queue() {
      obj = dequeue($hatch_queue)
      if obj.color == HATCH        // 思考,什么时候不为hatch?
        paint_gray(obj)
        scan_gray(obj)
        collect_white(obj)
      else if !is_empty($hatch_queue)
        scan_hatch_queue()
    }
    继续看下一个关键中的关键,下面这个是个递归函数。它的核心思想在于,如果当前这个obj是个循环垃圾,那么它的引用计数不为0的原因,是因为被垃圾循环引用着。同理,如果从它自己的子节点开始尝试着循环减引用计数,如果能减到自己为0,那么可以说明自己是循环引用的垃圾。
     
    paint_gray(obj) {
      // 递归函数
      if obj.color == BLACK | HATCH // 为什么可能为BLACK?因为起始对象虽然是hatch,但它的引用的子对象可能是black
        obj.color = GRAY                  // 标识,防止在循环引用的情况下无尽递归
        for child : children(obj)
          child.ref_cnt--                    // 注意!关键点!hatch的obj本身没有减,而是从子节点开始减!这个减是个试探减,最终如果不是循环引用垃圾,还要恢复!
          paint_gray(child)
    }
    经过上述处理,已经将可疑的hatch对象的子对象全部递归了一遍,以上是核心逻辑,下面则是最终判断,要为hatch定性:到底是不是循环引用垃圾?
    scan_gray(obj) {
      if obj.color == GRAY
        if obj.ref_cnt > 0
          paint_black(obj)        // 平反,因为如果真是循环引用垃圾,转一轮下来应该被引用的子对象回头来减过引用计数了
        else
          obj.color = WHITE   // 定罪,因为本身paint_gray时,并未减自身的计数,这里为0了,只可能是被引用的对象轮回回来减了,
          for child : children(obj)   // 既然本身已经确定是循环垃圾了,那么之前的尝试减有效,可以遍历子节点找出引用环了。
            scan_gray(obj)
    }
     
    最后,看一下“平反”的过程,很容易理解,在paint_gray中试减掉的引用计数要恢复回来:
     
    paint_black(obj) {
      obj.color = BLACK
      for child : children(obj)
        child.ref_cnt++              // 注意,这里也是当前对象没有加,从引用的子对象开始加。因为当证明当前非垃圾的情况下,当前对象当初也没有减
        if child.color != BLACK
          paint_black(child)         // 递归恢复
    }
    最后的最后,递归清理垃圾:
    collect_white(obj) {
      if obj.color == WHITE
        obj.color = BLACK  //防止循环不结束,并非真的非垃圾
        for child : children(obj)
          collect_whilte(child)
        reclaim(obj)             // 回收
    }
    上面这个算法虽然很精妙,但是毕竟遍历了3次对象:mark_gray, scan_gray, collect_whilte,最大暂停时间有影响。
     
    合并引用计数
    另一个需要讨论改良的,是引用计数的频繁变动的处理。比如a.pa = b; a.pa = c; a.pa = d; a.pa = a; a.pa = b这样,绕了半天,引用还是从a到b。
    考虑可以不关注过程,直接关注首尾结果,按这结果来生成一个阶段内的引用计数变化。
    write_barrier(obj, field, dst) {
      if !obj.dirty    // 引用源注册,注册,即是记录某一阶段起始的意思
        register(obj)
     
      obj.field = dst
    }
     
    register(obj) {
      if buff_full
        fail()
      // entry有两个字段,一个记录obj,另一个数组记录obj当前的所有引用子对象
      entry.obj = obj
      for child: children(obj)
        if child
          push(entry.children, child)
     
      push($buf, entry)      // entry存在buff中
      obj.dirty = true         // 完毕
    }
    可以看出,entry中,obj表明的是一个一直随着代码运行在变化的引用关系,而entry.children这个队列,则是保存着刚开始register时,obj的引用关系。
    显然,gc逻辑就是两者对比:
    garbage_collect() {
      for entry : buf
        obj = entry.obj
        for child : children(obj)
          inc_ref_cnt(child)               // 当前被引用的,+1
     
        for child : entry.children
          dec_ref_cnt(child)              // 曾经被引用的, -1。如果引用未变,那就不增不减,维持原样。
     
        obj.dirty = false
     
      clear(buf)
    }
    这方法对于频繁更新指针的情况能增加吞吐量,但因为要处理buf,他会加大暂停的时间。
     
    引用计数的内容就是这些。
  • 相关阅读:
    单例模式
    HashSet、LinkedHashSet、SortedSet、TreeSet
    ArrayList、LinkedList、CopyOnWriteArrayList
    HashMap、Hashtable、LinkedHashMap
    andrew ng machine learning week8 非监督学习
    andrew ng machine learning week7 支持向量机
    andrew ng machine learning week6 机器学习算法理论
    andrew ng machine learning week5 神经网络
    andrew ng machine learning week4 神经网络
    vue组件监听属性变化watch方法报[Vue warn]: Method "watch" has type "object" in the component definition. Did you reference the function correctly?
  • 原文地址:https://www.cnblogs.com/qqmomery/p/6629524.html
Copyright © 2011-2022 走看看