引用计数算法在每个对象都维护着一个内存字段来统计它被多少”部分”使用—引用计数器,每当有一个新的引用指向该对象时,引用计数器就+1 ,每当指向该引用对象失效时该计数器就-1 ,当引用数量为0的时候,则说明对象没有被任何引用指向,可以认定是”垃圾”对象.
由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象;但也因为缺乏全局对象图信息,所以无法处理循环引用的状况。更高级的引用计数实现会引入“弱引用”的概念来打破某些已知的循环引用,但那是另一个话题了。(By RednaxelaFX)
至于存放引用计数器的位置放在哪? RednaxelaFX介绍了两种方案. 可以侵入式的存在对象内,例如CPython就把引用计数存在每个受自动内存管理的Python对象的对象头里(PyObject的ob_refcnt字段),或者COM的IUnknown::AddRef()/Release();也可以非侵入式的存在对象外面,例如C++11标准库里的std::shared_ptr。
下面通过网上一段非常常见的代码来分析为什么会产生循环引用的问题,目前只看到Gityuan有一段说明感觉是非常清晰地说明了这段代码:
public static void main(String[] args) { GcObject obj1 = new GcObject(); //Step1 GcObject obj 2 = new GcObject();//Step2 obj1.instance = obj2; //Step3 obj2.instance = obj1;// //Step4 obj1 = null; //Step5 obj2 = null; //Step6 }
当采用引用计数算法时:
- 第一步:GcObject实例1被obj1引用,所以它的引用数+1,为1
- 第二步:GcObject实例2被obj2引用,所以它的引用数+1,为1
- 第三步:obj1的instance属性指向obj2,而obj2指向GcObject实例2,故GcObject实例2引用+1,为2
- 第四步:obj2的instance属性指向obj1,而obj1指向GcOjbect实例1,故GcObject实例1引用+1,为2
到此前4步, GcOjbect实例1和GcOjbect实例2的引用数量均为2,此时结果图如下.
PS:注意想一下,为什么是obj的instance属性,而不是写成obj本身?
5.第五步:obj1不再指向GcOjbect实例1,其引用计数减1,结果为1.
6.第六步:obj2不再指向GcOjbect实例2,其引用计数减1,结果为1.
到此,发现GcObject实例1和实例2的计数引用都不为0,那么如果采用的引用计数算法的话,那么这两个实例所占的内存将得不到释放,这便产生了内存泄露。
引用计数算法常用来说明垃圾回收算法的机制,但是很少为各种语言所有,其中COM采用的就是引用计数算法.
前面引用RednaxelaFX的话时,他提到过一种高级的引用计数算法采用了”弱引用”来解决了部分已知的循环引用.弱引用请看另一篇转载的文章.
参考文档:
https://www.zhihu.com/question/21539353
《CLR via C#》(第4版)