给每个block引入‘计数器’,表示对象的引用次数
计数器的增减
- 在申请内存时会修改计数器:
new_obj(size){
obj = pickup_chunk(size, $free_list)
//引用计数法中所有空闲对象都在free_list中
if(obj == NULL)
allocation_fail()
else
//申请内存成功会更新为1
obj.ref_cnt = 1
return obj
}
- update_ptr()用于更新指针ptr,使其指向对象obj,同时进行计数的增减
update_ptr(ptr, obj){
//对指针 ptr 新引用的对象(obj)的计数器进行增量操作
inc_ref_cnt(obj)
//对指针 ptr 之前引用的对象(*ptr)的计数器进行减量操作
dec_ref_cnt(*ptr)
// 指针ptr指向新的obj
*ptr = obj
}
inc_ref_cnt(obj){
obj.ref_cnt++
}
dec_ref_cnt(obj){
obj.ref_cnt--
if(obj.ref_cnt == 0)
for(child : children(obj))
dec_ref_cnt(*child)
//将obj连接到free_list
reclaim(obj)
}
优点
- 可即刻回收垃圾:当计数值为0时,会马上回收对象到free_list,提高内存使用效率
- 最大暂停时间短:此方法只有当mutator更新指针时才会执行垃圾回收,有效减少暂停时间
- 没必要从根指针开始查找,减少查询时间。比如在分布式系统中会再节点内用标记-清除算法,节点间的引用时则用引用计数法。
缺点
- 计数器的增减操作频繁
- 计数器需要占用一定的内存
- 实现繁琐,*ptr=obj的地方要重写成update_ptr()
- 循环引用无法回收
延迟引用计数法
目的是改善计数器值的增减处理繁重的问题。
使用一个ZCT(Zero Count Table)来记录计数器值在dec_ref_cnt()函数作用下变为0的对象
对应的dec_ref_cnt(),new_obj()方法:
dec_ref_cnt(obj){
obj.ref_cnt--
if(obj.ref_cnt == 0)
if(is_full($zct) == TRUE)
//zct满了,扫描去除空闲的block
scan_zct()
//将当前的对象写入ZCT
push($zct, obj)
}
new_obj(size){
obj = pickup_chunk(size, $free_list)
if(obj == NULL)
scan_zct()
obj = pickup_chunk(size, $free_list)
if(obj == NULL)
allocation_fail()
obj.ref_cnt = 1
return obj
}
对应的scan_zct()函数,找到当前空闲的block,清除并放到free_list 中
scan_zct(){
for(r : $roots)
(*r).ref_cnt++
for(obj : $zct)
if(obj.ref_cnt == 0)
remove($zct, obj)
delete(obj)
for(r : $roots)
(*r).ref_cnt--
}
//先递归减少子对象的计数,如果为0则继续释放,重新挂载到free_list上
delete(obj){
for(child : children(obj)
(*child).ref_cnt--
if((*child).ref_cnt == 0)
delete(*child)
reclaim(obj)
}
优点
- 减少计数器增减的频繁调用
缺点
- 垃圾不能马上被回收,失去可即刻回收垃圾的优点
- scan_zct()会加大最大暂停时间,时间长度和ZCT的长度成正比
Sticky引用计数法
使用引用计数法时要考虑的一个问题就是计数器的位宽多大合适,大了比较消耗空间,小了又可能溢出。
解决方法:
什么也不做
这样是有溢出的风险,可能会内存泄漏,但是一般计数器都在0-1变化,溢出的场景很少
用标记-清除算法来管理
不仅可以解决溢出的问题,还能解决循环引用的问题。缺点就是标记-引用算法的缺点【】
1位引用计数法
not write
部分标记-清除算法
not write