您的位置:首页 > 其它

引用计数法

2016-12-17 18:55 239 查看
引用计数法是在mutator(应用程序)的启动过程中通过增减计数器的值来进行内存管理

涉及到new_obj和update_ptr函数

//在mutator生成新的对象的时候会调用new_obj函数

new obj(size){

      obj = pickup_chunk(size,$free_list)


      if(obj == NULL)

         allocation_fail()   //pickup_chunk返回NULL的时候,分配就失败了,也就是没有大小合适的分块了

      else

         obj.ref_count = 1  //ref_count是obj的计数器

     return obj

}


update_ptr(ptr,obj){

inc_ref_cnt(obj)   //对指针新引用的对象obj的计数器进行增量操作

dec_ref_cnt(*ptr) //对ptr之前引用的对象obj计数器进行减量操作

*ptr = obj

}


为什么先执行inc_ref_cnt(obj)的操作,在进行dec的操作呢 ?这是为了处理*ptr和obj是统一对象的情况,如果先执行dec操作的话,*ptr的值的值就可能变为0而被回收,再执行inc时对象就已经不存在了,会引发重大的bug

优点:

1)可即刻回收垃圾,每个对象都知道自己的被引用数ref_count,当ref_count为0时,对象就会把自己作为空闲空间连接到空闲链表,也就是,在对象变成垃圾的同时就会被回收,而其它的GC算法只有当分块用尽GC开始执行时,才会知道哪个是垃圾,也就是GC之前会有一部分内存被垃圾占用

2)最大暂停时间短,每次通过指向mutator生成垃圾时,这部分垃圾都会被回收,大幅削减了mutator的最大暂停时间

3)没必要沿指针查找,当我们需要减少沿指针查找的次数是,它就派上用场了

eg:在分布式环境中,如果要沿各个计算节点之间的指针进行查找,成本就会增大,所以要极力减少沿指针查找的次数

缺点 :

1)计数器的值增减处理繁重每次指针更新时,计数器的值会被更新,所以在频繁更新指针的mutator中,值的增减处理会变得繁重

2)计数器要占很多位,假如32位的机器,就有可能2的32次方个对象同时引用一个对象,所以必须确保各对象的计数器有32位大小,也就是对于所有的对象,必须留有32位的空间,使内存使用大大降低

3)实现复杂,算法本身简单,但实现复杂,比方说,需要把以往写成*ptr = obj 的地方都要重新写成 update_ptr(ptr,obj)因为调用update_ptr的地方很多,重写过程极易出错

4)循环引用无法回收,就是两个对象相互引用的情况 比如两个对象是同一个类的实例,属性相互赋值

### 延迟引用计数法,针对引用计数法的缺点1提出的改进方法

缺点1导致的原因之一是从根的引用变化频繁,因此把根引用的变化不反应在计数器上,这样频繁重写堆中的变量指针时,对象的指针值也不会有变化,但因为引用没有反应到计数器,会导致有的对象仍在活动却被当成垃圾回收掉,于是,采用ZCT(Zero Count Table),它是一个表,会记录下在dec_ref_count的作用下变为0的对象,暂时保留,修正dec_ref_count函数,适应延迟引用计数法:

dec_ref_count(obj){

obj.ref_count--

if(obj.ref_count == 0)  //若zct爆满,先用scan_zct减少$zct中的对象

if(is_null($znt) == true)

scan_zct()

push($zct,obj)  //当obj的引用计数为0时添加到zct

}


修正new_obj函数

new_obj(size){

obj = pickup_chunk(size,$free_list)

if(obj == NULL) //若第一次分配失败,意味着空闲链表没有合适的块,

scan_zct()  //搜索一遍zct

  obj = pickup_chunk(size,$free_list) //再次分配

if(obj == NULL)   //依然不行

allocation_fail()  //就分配失败了

obj.ref_cnt = 1

return obj

}


来看下scan_zct函数

scan_zct(){

for(r : $roots)

(*r).ref_count++ //把所有通过根直接引用的对象的计数器都进行增量,才把根引用反映到计数器上


for(obj:$zct) //检查与zct相连的对象,若ref_cnt=0,对子对象的计数器减量,并将其回收

if(obj.ref_cnt == 0)

remove($zct,obj)

delete(obj)


for(r : $roots)

(*r).ref_cnt--

}


优点:延迟了根引用的计数,将垃圾一并回收,通过延迟,减少了根引用频繁变化导致计数器增减带来的额外负担

缺点:垃圾不能马上回收,失去了引用计数的一大优点:可即刻回收垃圾

2)scan_cnt使最大暂停时间延长,执行scan_cnt的时间与zct的大小成正比,zct大,妨碍mutator的时间就长,若缩减zct,又使得调用scan_cnt的频率增加,压低吞吐量,很显然本末倒置了

### sticky引用计数法:

计数器占一个字的空间或大大的浪费内存,可以减少计数器的位宽,假设为5位,也就是最多引用32-1次,超过31个对象引用计数器就会溢出,但是,引用对象超过31次的对象肯定是活跃对象,不操作,也不会有大的问题(对溢出不处理)

使用GC标记-清除算法管理:

标记阶段:把根直接引用的对象堆到标记栈里,按顺序从标记栈取出对象,对计数器进行增量操作,不过,必须把各对象只标记一次

清除阶段,会搜索整个堆,回收计数器为0的对象

与之前的标记清除算法不同:

1)一开始就把所有对象的计数器的值设置为0

2)不标记对象,而是对对象的计数器进行增减操作

3)为了对计数器增量操作,对活动对象多次搜索

所以,要是把sticky法和GC标记-清除法一起使用,在计数器溢出后程序还是能回收,还可以回收循环垃圾

但是,在标记之前必须重置所有的对象和计数器,且由于查找对象时需要多次查找,耗时更多,吞吐量会降低

### 1位引用计数法

是sticky引用计数法的极端情况,计数器只有1位

它称为标签flag更合适,引用对象为1时,标签为unique,大于1时,为multiple

优点:最不容易出现高速缓存缺失,节省内存消耗

缺点:计数器溢出


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: