大家好,我是【小松与蘑菇】,即将毕业去深圳的大学生,致力于android,java相关领域,也对AI很感兴趣。正朝着写出通俗易懂而又有深度的文章而努力
前文地址
【GC算法几人知?】一、前置知识积累
【GC算法几人知?】二、标记清除法 全解析
一个追根溯源的问题:哪些对象算是垃圾对象?
答曰:从根开始无法引用的对象
GC方法此时已经显而易见——判定一个对象是否根可引用,如果不行,就是垃圾,进行回收
这就是引用计数法
整个方法,我们关注的,是计数器,类似于标记清除法中的mark,我们在对象头中设置一个cnt,不过,这个是int型
当新建一个对象A时,就初始化cnt为1
以下两种情况会更新:
update_cnt(ptr,object)
上面两种情况实际上是一种情况,这是一种你增我减的情况,当出现指针指向A时,马上执行下面的函数
update_ptr(ptr, obj){
inc_ref_cnt(obj)
dec_ref_cnt(*ptr)
*ptr = obj
}
可能你会有点奇怪,照理来说,应该是 *ptr的对象先cnt–,obj才能cnt++,但是这样会有问题,如果 *ptr就是obj呢?也就是ptr原本指向的就是obj,然后重新再指向一遍的话,那么先cnt–将有可能使得cnt为0,就成为了垃圾对象而惨遭回收,所以必须先加
inc_ref_cnt
函数仅仅是对传入的对象进行cnt++而已,很简单,dec_ref_cnt
函数由于承载了回收的功能,还要考虑对子对象的递归判定,实现较为复杂,这也是整个引用计数法中最重要的函数
dec_ref_cnt(obj){
obj.ref_cnt--
if(obj.ref_cnt == 0)
for(child : children(obj))
dec_ref_cnt(*child)
reclaim(obj)
}
可以看到,cnt–操作下,也同时可以回收垃圾
这个方法就讲完了,是不是很简单?嘻嘻,才不是,不过到这里,其实你已经可以跟面试官扯皮很久了,下面是更加深入的解析
来我们先看看有什么优点
成也计数,败也计数
缺点也很明显
dec_ref_cnt
这个函数时,被cnt–的对象如果减到0,那么其子节点将会递归的受到牵连cnt–,最坏情况下,修改一个指针,所有对象的cnt都得减,实在太耗费资源了log(堆中最大对象数)
才是最安全的作为一个不屈不挠的程序员,我们的前辈们当然想办法解决了这些问题啦
由于计数是实时且递归更新的,往往会有牵一发而动全身的情况,所以我们设置一个ZCT表(像不像上篇的位图!)
什么时候会发生递归?是当前被cnt–的对象cnt为0时,那么我们只需要此时将这个对象链入ZCT表中记录一下即可
这样,即使出现cnt==0的情况,也无需递归更新其子对象了,ZCT表的大小可以自己定,如果ZCT满了,那么再统一清除表内指向的对象们
这体现一个思想:对于多次变化的东西,我们可以建个表,记录清楚,然后一次性变化即可
不过这也导致了无法即时回收垃圾,如果ZCT表越大,单次回收的时间也越长,需要考虑清楚再设置
计数器的位数一定不能是 log(最大对象数)
的,不然每个对象的头都大了(计数器在对象的头中),所以一定会有溢出问题,一个简单的方法是,检测到将要溢出,计数器不再增加即可。因为如果一个对象的计数器大到将要溢出,证明又很多对象指向他,所以大概率不会是垃圾,以后也不会是
当然这个方法还是有风险的,所以我们可以使用Stick引用计数
如果一个计数器溢出,先用类似ZCT的表存一下,然后满了之后再将所有溢出的计数器置为0,重新从根开始遍历,如果有引用则cnt++(不管引用多少次,只加一次),最后谁的cnt为0,谁被回收
简单说就是将所有溢出的存一批,然后全设置为0,再遍历一次,如果有对象居然没有cnt++,证明他溢出后,所有之前指向他的指针都不再指向他了,证明他败光了所有的财产,成为了垃圾(全设置为0,就像让潮水退去一样,看看谁打着溢出的名号,其实没穿底裤)。
注意:这个方法过于硬核,需要保持10分钟专注
我先给个图
使用两位,将对象分成了四种颜色,逻辑是这样的
新建的对象肯定是黑色的
如果出现某对象cnt–
其伪代码如下dec_ref_cnt
变成
dec_ref_cnt(obj){
obj.ref_cnt--
if(obj.ref_cnt == 0)
delete(obj)
else if(obj.color != HATCH)
obj.color = HATCH
enqueue(obj, $hatch_queue)
}
当hatch_queue不为空
方便理解,如下图,从hatch_queue中取出A,此时假设A中cnt==1,不是垃圾,但是我们明显看出,A,B,C有循环引用
然后让A的子对象B进行cnt–,此时B为0,然后继续递归,对B的子对象C进行cnt–,使得C为0,C继续对子对象A和F进行cnt–
发现了吗?
原来的A被减为0了
这意味着什么?
这意味着A的cnt数值来自于其子对象的指向,而不是根,他和那个指向他的子对象,和他两之间的对象,是循环引用(注意F不是,循环在C处回到了A)
然后对循环引用的对象涂白色,表明他们是垃圾对象
最后,这个方法有很多的实现细节,以后细讲,本文主要讲逻辑和必须的代码
不过他也有缺点
引用计数法作为直抵GC本质的方法实现起来是顺理成章的,也是最能直观理解的GC算法,但是对于计数的操作却有很多很多的问题,当然,与之对应的解决方法也很精彩,后面的垃圾回收算法也更加精彩哦