天天看点

垃圾回收-- 引用计数法

引用计数法

引用计数法,最重要的就是计数器,记录有多少引用该对象

引用计数法与mutator 的执行密切相关,在mutator 的处理过程中通过增减计数器的值来进行内存管理, 在分配和更新对象时会发生计数的变化

更新操作过程:

  1. 对指针ptr 新引用的对象(obj)的计数器进行增量操作
  2. 对指针ptr 之前引用的对象(*ptr)的计数器进行减量操作
update_ptr(ptr, obj){
   inc_ref_cnt(obj)
   dec_ref_cnt(*ptr)
   *ptr = obj
}
           

注: 为了处理*ptr 和obj 是同一对象时的情况,向增加,后递减, 防止对象在发生位置改变时,还没有改变就已经被回收了

更新指针,可能会产生没有被任何程序引用的垃圾对象。引用计数法中会监督在更新指针的时候是否有产生垃圾,从而在产生垃圾时将其立刻回收

特点:

  1. 可即刻回收垃圾
  2. 最大暂停时间短 因为不用扫描全部堆,因此时间节省量
  3. 计数器值的增减处理繁重 严重依赖计数器
  4. 计数器需要占用很多位 假如我们用的是32 位机器,那么就有可能要让2 的32 次方个对象同时引用一个对象, 计数器有32 位大小
  5. 实现烦琐复杂 对于每次对象的变更都需要进行精确的维护, 否则会造成内存无法会回收, 即内存溢出
  6. 循环引用无法回收

改进:

  1. 延迟引用计数法

在延迟引用计数法中使用ZCT(Zero Count Table)。ZCT 是一个表,它会事先

记录下计数器值在dec_ref_cnt() 函数的作用下变为0 的对象, 因为计数器值为0 的对象不一定都是垃圾,所以暂时先将这些对象保留

在延迟引用计数法中,程序延迟了根引用的计数,将垃圾一并回收。通过延迟,减轻了

因根引用频繁发生变化而导致的计数器增减所带来的额外负担。当然也失去量垃圾即可回收的特点

  1. Sticky 引用计数法

是用来减少计数器位宽

. 什么都不做 很多情况下,计数器的值会在0 到1 的范围内变化,鲜少出现5 位计数器溢出这样的情况 , 不增减计数器的值,就把它那么放着也不会有什么大问题

. 使用GC 标记- 清除算法进行管理

  1. 1 位引用计数法

计数器只有1 位大小,特点总结是: 几乎没有对象是被共有的,所有对象都能被马上回收。指针通常默认为4 字节对齐,所以没法利用低2 位。只要好好利用这个性质,就能确保拿出1 位来用作内存管理

1 位引用计数法的优点,是不容易出现高速缓存缺

可能会出现很多对象计数器溢出的情况

  1. 部分标记- 清除算法

采用一般情况下执行引用计数法,在某个时刻启动GC 标记- 清除算法的方法。

“有循环引用的垃圾”,一般来说这种垃圾应该很少,单纯的GC 标记- 清除算法又是以全部堆为对象的,所以会产生许多无用的搜索。只对“可能有循环引用的对象群”使用GC 标记- 清除

算法,对其他对象进行内存管理时使用引用计数法。像这样只对一部分对象群使用GC 标记-

清除算法的方法,叫作“部分标记- 清除算法”(Partial Mark & Sweep)

部分标记- 清除算法中,对象会被涂成4 种不同的颜色来进行管理

  1. 黑(BLACK):绝对不是垃圾的对象(对象产生时的初始颜色)
  2. 白(WHITE):绝对是垃圾的对象
  3. 灰(GRAY):搜索完毕的对象
  4. 阴影(HATCH):可能是循环垃圾的对象

头中分配2 位空间,然后用00~11 的值对应这4 个颜色

部分标记- 清除算法的优点,就是把要搜索的对象限定在阴影对象及其子对象,也就是

“可能是循环垃圾的对象群”中

循环垃圾产生过程: 初始状态下根引用对象A,对象A 引用对象B,对象B 引用对象C。接下来我们创建一个从对象C 到对象A 的引用。在这里就形成了A → B → C → A 的循环引用。最后我们删除从根到对象A 的引用。这样一来,对象A 到C 的循环引用对象群就成了垃圾

条件:

  1. 产生循环引用
  2. 删除从外部到循环引用的引用

部分标记- 清除算法中用dec_ref_cnt() 函数来检查这个值。如果对象的计数器值减量

后不为0,说明这个对象可能是循环引用的一份子。这时会先让这个对象连接到队列,以方

便之后搜索它

标记- 清除算法并不是完美的,因为从队列搜索对象所付出的成本太大了。

被队列记录的对象毕竟是候选垃圾,所以要搜索的对象绝对不在少数。这个算法总计需要

查找3 次对象,也就是说需要对从队列取出的阴影对象分别执行1 次mark_gray() 函数、

scan_gray() 函数以及collect_white() 函数。这大大增加了内存管理所花费的时间, 因此引用计数的优势- 最大暂停时间就没有

继续阅读