C++中垃圾回收機制中幾種經典的垃圾回收算法

前言

垃圾收集器是一種動態存儲分配器,它自動釋放程序不再需要的已分配的塊,這些塊也稱爲 垃圾 。在程序員看來,垃圾就是不再被引用的對象。自動回收垃圾的過程則稱爲 垃圾收集(garbage collection) 。在一個支持垃圾收集的語言中,程序顯式地申請內存,但從不需要顯式的釋放它們。垃圾收集器會定期識別垃圾塊,並將垃圾塊放回空閒鏈表中。顯然,C語言的malloc包不是一個帶GC功能的分配器,程序員顯式 調用malloc分配內存,也需要顯式調用free釋放它。而像java、C#這些語言等則提供了垃圾收集器。

基本概念

有向可達圖與根集

垃圾收集器將存儲器視爲一張有向可達圖。圖中的節點可以分爲兩組:一組稱爲根節點,對應於不在堆中的位置,這些位置可以是寄存器、棧中的變量,或者是虛擬存儲器中讀寫數據區域的全局變量;另外一組稱爲堆節點,對應於堆中一個分配塊,如下圖:

這裏寫圖片描述

當存在一個根節點可到達某個堆節點時,我們稱該堆節點是可達的,反之稱爲不可達。不可達堆節點爲垃圾。可見垃圾收集的目標即是從從根集出發,尋找未被引用的堆節點,並將其釋放。

三種基本的垃圾收集算法及其改進算法

垃圾收集算法是一個重要而活躍的研究領域,自從20世紀60年代開始對垃圾收集進行研究以來,垃圾算法的研究從未停止。常見的垃圾收集算法有一下這幾種類型:

1、引用計數算法

引用技術算法是唯一一種不用用到根集概念的GC算法。其基本思路是爲每個對象加一個計數器,計數器記錄的是所有指向該對象的引用數量。每次有一個新的引用指向這個對象時,計數器加一;反之,如果指向該對象的引用被置空或指向其它對象,則計數器減一。當計數器的值爲0時,則自動刪除這個對象。

這裏寫圖片描述

缺點二是多個線程同時對引用計數進行增減時,引用計數的值可能會產生不一致的問題,必須使用併發控制機制解決這一問題,也是一個不小的開銷。

2、 Mark & Sweep 算法

這個算法也稱爲標記清除算法,爲McCarthy獨創。它也是目前公認的最有效的GC方案。Mark&Sweep垃圾收集器由標記階段和回收階段組成,標記階段標記出根節點所有可達的對節點,清除階段釋放每個未被標記的已分配塊。典型地,塊頭部中空閒的低位中的一位用來表示這個塊是否已經被標記了。通過Mark&Sweep算法動態申請內存時,先按需分配內存,當內存不足以分配時,從寄存器或者程序棧上的引用出發,遍歷上述的有向可達圖並作標記(標記階段),然後再遍歷一次內存空間,把所有沒有標記的對象釋放(清除階段)。因此在收集垃圾時需要中斷正常程序,在程序涉及內存大、對象多的時候中斷過程可能有點長。當然,收集器也可以作爲一個獨立線程不斷地定時更新可達圖和回收垃圾。該算法不像引用計數可對內存進行即時回收,但是它解決了引用計數的循環引用問題,因此有的語言把引用計數算法搭配Mark & Sweep 算法構成GC機制。

這裏寫圖片描述

3、 節點複製算法

Mark & Sweep算法的缺點是在分配大量對象時,且對象大都需要回收時,回收中斷過程可能消耗很大。而節點複製算法則剛好相反,當需要回收的對象越多時,它的開銷很小,而當大部分對象都不需要回收時,其開銷反而很大。
算法的基本思路是這樣的:從根節點開始,被引用的對象都會被複制到一個新的存儲區域中,而剩下的對象則是不再被引用的,即爲垃圾,留在原來的存儲區域。釋放內存時,直接把原來的存儲區域釋放掉,繼續維護新的存儲區域即可。過程如圖:

這裏寫圖片描述

可以看到,當被引用對象(非垃圾對象)很多時,需要複製很多的對象到新存儲區域。

分代回收

以上三種基本算法各有各的優缺點,也各自有許多改進的方案。通過對這三種方式的融合,出現了一些更加高級的方式。而高級GC技術中最重要的一種爲分代回收。它的基本思路是這樣的:程序中存在大量的這樣的對象,它們被分配出來之後很快就會被釋放,但如果一個對象分配後相當長的一段時間內都沒有被回收,那麼極有可能它的生命週期很長,嘗試收集它是無用功。爲了讓GC變得更高效,我們應該對剛誕生不久的對象進行重點掃描,這樣就可以回收大部分的垃圾。爲了達到這個目的,我們需要依據對象的”年齡“進行分代,剛剛生成不久的對象劃分爲新生代,而存在時間長的對象劃分爲老生代,根據實現方式的不同,可以劃分爲多個代。

一種回收的實現策略可以是:首先從根開始進行一次常規掃描,掃描過程中如果遇到老生代對象則不進行遞歸掃描,這樣可大大減少掃描次數。這個過程可使用標記清除算法或者複製收集算法。然後,把掃描後殘留下來的對象劃分到老生代,若是採用標記清除算法,則應該在對象上設置某個標誌位標誌其年齡;若是採用複製收集,則只需要把新的存儲區域內對象設置爲老生代就可以了。而實際的實現上,分代回收算法的方案五花八門,常常會融合幾種基本算法。

而其他的改進算法數量非常龐大,但大都基於上述的三種基本算法。

C++垃圾回收機制

C語言本身沒有提供GC機制,而C++ 0x則提供了基於引用計數算法的智能指針進行內存管理。也有一些不作爲C++標準的垃圾回收庫,如著名的Boehm庫。藉助其他的算法也可以實現C/C++的GC機制,如前面所說的標記清除算法。

這裏寫圖片描述

當應用程序使用malloc試圖從堆上獲得內存塊時,通常都是以常規方式來調用malloc,而當malloc找不到合適空閒塊的時候,它就會去調用垃圾收集器,以回收垃圾到空閒鏈表。此時,垃圾收集器將識別出垃圾塊,並通過free函數將它們返回給堆。這樣看來,垃圾收集器代替我們調用了free函數,從而讓我們顯式分配,而無須顯式釋放。

上圖中的垃圾收集器爲一個保守的垃圾收集器。保守的定義是:每個可達的塊都能夠正確地被標記爲可達,而一些不可達塊卻可能被錯誤地標記爲可達。其根本原因在於C/C++語言不會用任何類型信息來標記存儲器的位置,即對於一個整數類型來說,語言本身沒有一種顯式的方法來判斷它是一個整數還是一個指針。因此,如果某個整數值所代表的地址恰好的某個不可達塊中某個字的地址,那麼這個不可達塊就會被標記爲可達。所以,C/C++所實現的垃圾收集器都不是精確的,存在着回收不乾淨的現象。而像JAVA的垃圾收集器則是精確回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章