引用计数法是在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
优点:最不容易出现高速缓存缺失,节省内存消耗
缺点:计数器溢出