引用计数法

引用计数法是在mutator(应用程序)的启动过程中通过增减计数器的值来进行内存管理

涉及到new_obj和update_ptr函数

 
   
  1. //在mutator生成新的对象的时候会调用new_obj函数
  2. new obj(size){
  3.       obj = pickup_chunk(size,$free_list)
  4.       if(obj == NULL)
  5.          allocation_fail()   //pickup_chunk返回NULL的时候,分配就失败了,也就是没有大小合适的分块了
  6.       else
  7.          obj.ref_count = 1 //ref_count是obj的计数器
  8.      return obj
  9. }

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

为什么先执行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函数,适应延迟引用计数法:

 
   
  1. dec_ref_count(obj){
  2. obj.ref_count--
  3. if(obj.ref_count == 0) //若zct爆满,先用scan_zct减少$zct中的对象
  4. if(is_null($znt) == true)
  5. scan_zct()
  6. push($zct,obj) //当obj的引用计数为0时添加到zct
  7. }

修正new_obj函数

 
   
  1. new_obj(size){
  2. obj = pickup_chunk(size,$free_list)
  3. if(obj == NULL) //若第一次分配失败,意味着空闲链表没有合适的块,
  4. scan_zct() //搜索一遍zct
  5. obj = pickup_chunk(size,$free_list) //再次分配
  6. if(obj == NULL) //依然不行
  7. allocation_fail() //就分配失败了
  8. obj.ref_cnt = 1
  9. return obj
  10. }

来看下scan_zct函数

 
   
  1. scan_zct(){
  2. for(r : $roots)
  3. (*r).ref_count++ //把所有通过根直接引用的对象的计数器都进行增量,才把根引用反映到计数器上
  4. for(obj:$zct) //检查与zct相连的对象,若ref_cnt=0,对子对象的计数器减量,并将其回收
  5. if(obj.ref_cnt == 0)
  6. remove($zct,obj)
  7. delete(obj)
  8. for(r : $roots)
  9. (*r).ref_cnt--
  10. }

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

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

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

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

缺点:计数器溢出





你可能感兴趣的:(GC垃圾回收的算法和实现)