引用計數算法在每個對象都維護着一個內存字段來統計它被多少”部分”使用—引用計數器,每當有一個新的引用指向該對象時,引用計數器就+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版)