第一節. gc的演算法與工作方式
1.演算法
垃圾收集器的本質,就是跟蹤所有被引用到的物件,整理物件不再被引用的物件,**相應的記憶體。
這聽起來類似於一種叫做「引用計數(reference counting)」的演算法,然而這種演算法需要遍歷所有物件,並維護它們的引用情況,所以效率較低些,並且在出現「環引用」時很容易造成記憶體洩露。所以.net中採用了一種叫做「標記與清除(mark sweep)」演算法來完成上述任務。
「標記與清除」演算法,顧名思義,這種演算法有兩個本領:
「標記」本領——垃圾的識別:從應用程式的root出發,利用相互引用關係,遍歷其在heap上動態分配的所有物件,沒有被引用的物件不被標記,即成為垃圾;存活的物件被標記,即維護成了一張「根-物件可達圖」。
其實,clr會把物件關係看做「樹圖」,無疑,了解資料結構的同學都知道,有了「樹圖」的概念,會加快遍歷物件的速度。
檢測、標記物件引用,是一件很有意思的事情,有很多方法可以做到,但是只有一種是效率最優的,.net中是利用棧來完成的,在不斷的入棧與出棧中完成檢測:先在樹圖中選擇乙個需要檢測的物件,將該物件的所有引用壓棧,如此反覆直到棧變空為止。棧變空意味著已經遍歷了這個區域性根(或者說是樹圖中的節點)能夠到達的所有物件。樹圖節點範圍包括區域性變數(實際上區域性變數會很快被**,因為它的作用域很明顯、很好控制)、暫存器、靜態變數,這些元素都要重複這個操作。一旦完成,便逐個物件地檢查記憶體,沒有標記的物件變成了垃圾。
「清除」本領——**記憶體:啟用compact演算法,對記憶體中存活的物件進行移動,修改它們的指標,使之在記憶體中連續,這樣空閒的記憶體也就連續了,這就解決了記憶體碎片問題,當再次為新物件分配記憶體時,clr不必在充滿碎片的記憶體中尋找適合新物件的記憶體空間,所以分配速度會大大提高。但是大物件(large object heap)除外,gc不會移動乙個記憶體中巨無霸,因為它知道現在的cpu不便宜。通常,大物件具有很長的生存期,當乙個大物件在.net託管堆中產生時,它被分配在堆的乙個特殊部分中,移動大物件所帶來的開銷超過了整理這部分堆所能提高的效能。
compact演算法除了會提高再次分配記憶體的速度,如果新分配的物件在堆中位置很緊湊的話,快取記憶體的效能將會得到提高,因為一起分配的物件經常被一起使用(程式的區域性性原理),所以為程式提供一段連續空白的記憶體空間是很重要的。
2.代齡(generation)
代齡就是對heap中的物件按照存在時間長短進行分代,最短的分在第0代,最長的分在第2代,第2代中的物件往往是比較大的。generation的層級與framework版本有關,可以通過呼叫gc.maxgeneration得知。
通常,gc會優先收集那些最近分配的物件(第0代),這與作業系統經典記憶體換頁演算法「最近最少使用」演算法如出一轍。但是,這並不代表gc只收集最近分配的物件,通常,.net gc將堆空間按物件的生存期長短分成3代:新分配的物件在第0代(0代空間最大長度通常為256k),按位址順序分配,它們通常是一些區域性變數;第1代(1代空間最大長度通常為2 mb)是經過0代垃圾收集後仍然駐留在記憶體中的物件,它們通常是一些如表單,按鈕等物件;第2代是經歷過幾次垃圾收集後仍然駐留在記憶體中的物件,它們通常是一些應用程式物件。
當記憶體吃緊時(例如0代物件充滿),gc便被調入執行引擎——也就是clr——開始對第0代的空間進行標記與壓縮工作、**工作,這通常小於1毫秒。如果**後記憶體依然吃緊,那麼gc會繼續**第1代(**操作通常小於10毫秒)、第2代,當然gc有時並不是按照第0、1、2代的順序收集垃圾的,這取決於執行時的情況,或是手動呼叫gc.collect(i)指定**的代。當對第2代**後任然無法獲得足夠的記憶體,那麼系統就會丟擲outofmemoryexception異常
當經過幾次gc過後,0代中的某個物件仍然存在,那麼它將被移動到第1代。同理,第1、2代也按同樣的邏輯執行。
這裡還要說的是,gc heap中代的數量與容量,都是可變的(這由乙個「策略引擎」控制,在第二節中,會介紹到「策略引擎」), 以下**結合windbg可以說明這個問題,以下**中,可以通過單擊按鈕「button1」,不斷的分配記憶體,而後獲得物件「a」的代齡情況,並且在form載入時也會獲得「a」的代齡。
code
public partial class form1 : form
private void button1_click(object sender, eventargs e)
private void form1_load(object sender, eventargs e)
} 程式剛載入時,「a」的代齡為第0代,通過windbg我們還獲得了以下資訊:
可以看出,gc堆被分成了兩個段,三代,每代起始位址十進位制差值為12。
點選數次「button1」按鈕後,「a」的代齡公升為第2代,通過windbg我們又獲得了以下資訊:
這裡要注意乙個很關鍵的地方,就是各代的起始(generation x starts at)十進位制位址差值不再是12,0代與1代差為98904,1代與2代差為107908,這說明代的大小隨程式執行在改變,並且gc heap的大小也有變化。
GC演算法 筆記 GC標記 壓縮演算法
將 gc標記 清除演算法與gc複製演算法相結合。一 lisp2演算法 標記階段結束後進入壓縮階段,壓縮階段縮小被標記節點之間的距離。compaction phase set forwarding ptr adjust ptr move obj 優點 可有效利用堆 缺點 壓縮花費計算成本 二 two ...
GC演算法 複製演算法
複製演算法就是將記憶體空間二等分,每次只使用其中一塊.當執行gc時,講a部分的所有活動物件集體移到b中,就可以講a全部釋放.畫個圖就是 在執行gc前,記憶體長這樣 當執行gc後,記憶體就變成這樣了 還記得標記清除演算法的問題是什麼嗎?記憶體碎片化嚴重.現在好了,碎片化問題解決了,每次gc執行後,記憶...
GC複製演算法
gc複製演算法 它是把某一空間的活動物件全部複製到另乙個空間,複製完成後gc也就結束了 一起看下gc複製演算法的copying函式 copying copy函式在複製時會先檢查是否已被複製,若已被複製,不再操作,否則進行複製,貼上copied標籤複製完成返回新空間的位址,這樣即使有多個物件引用obj...