(轉)C#垃圾回收機制詳解

GC的前世與此生程序員

雖然本文是以.net做爲目標來說述GC,可是GC的概念並不是才誕生不久。早在1958年,由鼎鼎大名的圖林獎得主John McCarthy所實現的Lisp語言就已經提供了GC的功能,這是GC的第一次出現。Lisp的程序員認爲內存管理過重要了,因此不能由程序員本身來管理。但後來的日子裏Lisp卻沒有成氣候,採用內存手動管理的語言佔據了上風,以C爲表明。出於一樣的理由,不一樣的人卻又不一樣的見解,C程序員認爲內存管理過重要了,因此不能由系統來管理,而且譏笑Lisp程序慢如烏龜的運行速度。的確,在那個對每個Byte都要精心計算的年代GC的速度和對系統資源的大量佔用使不少人的沒法接受。然後,1984年由Dave Ungar開發的Small talk語言第一次採用了Generational garbage collection的技術(這個技術在下文中會談到),可是Small talk也沒有獲得十分普遍的應用。 
直到20世紀90年代中期GC才以主角的身份登上了歷史的舞臺,這不得不歸功於Java的進步,今日的GC已非吳下阿蒙。Java採用VM(Virtual Machine)機制,由VM來管理程序的運行固然也包括對GC管理。90年代末期.net出現了,.net採用了和Java相似的方法由CLR(Common Language Runtime)來管理。這兩大陣營的出現將人們引入了以虛擬平臺爲基礎的開發時代,GC也在這個時候愈來愈獲得大衆的關注。 
爲何要使用GC呢?也能夠說是爲何要使用內存自動管理?有下面的幾個緣由: 
一、提升了軟件開發的抽象度; 
二、程序員能夠將精力集中在實際的問題上而不用分心來管理內存的問題; 
三、可使模塊的接口更加的清晰,減少模塊間的偶合; 
四、大大減小了內存人爲管理不當所帶來的Bug; 
五、使內存管理更加高效。 
總的說來就是GC可使程序員能夠從複雜的內存問題中擺脫出來,從而提升了軟件開發的速度、質量和安全性。 
 算法

什麼是GC數據庫

GC如其名,就是垃圾收集,固然這裏僅就內存而言。Garbage Collector(垃圾收集器,在不至於混淆的狀況下也成爲GC)以應用程序的root爲基礎,遍歷應用程序在Heap上動態分配的全部對象[2],經過識別它們是否被引用來肯定哪些對象是已經死亡的哪些仍須要被使用。已經再也不被應用程序的root或者別的對象所引用的對象就是已經死亡的對象,即所謂的垃圾,須要被回收。這就是GC工做的原理。爲了實現這個原理,GC有多種算法。比較常見的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統.net CLR,Java VM和Rotor都是採用的Mark Sweep算法。安全

1、Mark-Compact 標記壓縮算法 
簡單把.NET的GC算法看做Mark-Compact算法 
階段1: Mark-Sweep 標記清除階段 
先假設heap中全部對象均可以回收,而後找出不能回收的對象,給這些對象打上標記,最後heap中沒有打標記的對象都是能夠被回收的 
階段2: Compact 壓縮階段 
對象回收以後heap內存空間變得不連續,在heap中移動這些對象,使他們從新從heap基地址開始連續排列,相似於磁盤空間的碎片整理 
alt
Heap內存通過回收、壓縮以後,能夠繼續採用前面的heap內存分配方法,即僅用一個指針記錄heap分配的起始地址就能夠 
主要處理步驟:將線程掛起=>肯定roots=>建立reachable objectsgraph=>對象回收=>heap壓縮=>指針修復 
能夠這樣理解roots:heap中對象的引用關係錯綜複雜(交叉引用、循環引用),造成複雜的graph,roots是CLR在heap以外能夠找到的各類入口點。GC搜索roots的地方包括全局對象、靜態變量、局部對象、函數調用參數、當前CPU寄存器中的對象指針(還有finalizationqueue)等。主要能夠歸爲2種類型:已經初始化了的靜態變量、線程仍在使用的對象(stack+CPU register) 
Reachable objects:指根據對象引用關係,從roots出發能夠到達的對象。例如當前執行函數的局部變量對象A是一個rootobject,他的成員變量引用了對象B,則B是一個reachable object。從roots出發能夠建立reachable objectsgraph,剩餘對象即爲unreachable,能夠被回收 
alt
指針修復是由於compact過程移動了heap對象,對象地址發生變化,須要修復全部引用指針,包括stack、CPUregister中的指針以及heap中其餘對象的引用指針 
Debug和release執行模式之間稍有區別,release模式下後續代碼沒有引用的對象是unreachable的,而debug模式下須要等到當前函數執行完畢,這些對象纔會成爲unreachable,目的是爲了調試時跟蹤局部對象的內容 
傳給了COM+的託管對象也會成爲root,而且具備一個引用計數器以兼容COM+的內存管理機制,引用計數器爲0時這些對象纔可能成爲被回收對象 
Pinnedobjects指分配以後不能移動位置的對象,例如傳遞給非託管代碼的對象(或者使用了fixed關鍵字),GC在指針修復時沒法修改非託管代碼中的引用指針,所以將這些對象移動將發生異常。pinnedobjects會致使heap出現碎片,但大部分狀況來講傳給非託管代碼的對象應當在GC時可以被回收掉 
2、 Generational 分代算法 
程序可能使用幾百M、幾G的內存,對這樣的內存區域進行GC操做成本很高,分代算法具有必定統計學基礎,對GC的性能改善效果比較明顯 
將對象按照生命週期分紅新的、老的,根據統計分佈規律所反映的結果,能夠對新、老區域採用不一樣的回收策略和算法,增強對新區域的回收處理力度,爭取在較短期間隔、較小的內存區域內,以較低成本將執行路徑上大量新近拋棄再也不使用的局部對象及時回收掉 
分代算法的假設前提條件: 
一、大量新建立的對象生命週期都比較短,而較老的對象生命週期會更長 
二、對部份內存進行回收比基於所有內存的回收操做要快 
三、新建立的對象之間關聯程度一般較強。heap分配的對象是連續的,關聯度較強有利於提升CPU cache的命中率 
.NET將heap分紅3個代齡區域: Gen 0、Gen 一、Gen 2 
alt
Heap分爲3個代齡區域,相應的GC有3種方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。若是Gen 0 heap內存達到閥值,則觸發0代GC,0代GC後Gen 0中倖存的對象進入Gen1。若是Gen 1的內存達到閥值,則進行1代GC,1代GC將Gen 0 heap和Gen 1 heap一塊兒進行回收,倖存的對象進入Gen2。2代GC將Gen 0 heap、Gen 1 heap和Gen 2 heap一塊兒回收 
Gen 0和Gen 1比較小,這兩個代齡加起來老是保持在16M左右;Gen2的大小由應用程序肯定,可能達到幾G,所以0代和1代GC的成本很是低,2代GC稱爲fullGC,一般成本很高。粗略的計算0代和1代GC應當能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時fullGC可能須要花費幾秒時間。大體上來說.NET應用運行期間2代、1代和0代GC的頻率應當大體爲1:10:100。app

 

3、Finalization Queue和Freachable Queue函數

這兩個隊列和.net對象所提供的Finalize方法有關。這兩個隊列並不用於存儲真正的對象,而是存儲一組指向對象的指針。當程序中使用了new操做符在Managed Heap上分配空間時,GC會對其進行分析,若是該對象含有Finalize方法則在Finalization Queue中添加一個指向該對象的指針。在GC被啓動之後,通過Mark階段分辨出哪些是垃圾。再在垃圾中搜索,若是發現垃圾中有被Finalization Queue中的指針所指向的對象,則將這個對象從垃圾中分離出來,並將指向它的指針移動到Freachable Queue中。這個過程被稱爲是對象的復生(Resurrection),原本死去的對象就這樣被救活了。爲何要救活它呢?由於這個對象的Finalize方法尚未被執行,因此不能讓它死去。Freachable Queue平時不作什麼事,可是一旦裏面被添加了指針以後,它就會去觸發所指對象的Finalize方法執行,以後將這個指針從隊列中剔除,這是對象就能夠安靜的死去了。.net framework的System.GC類提供了控制Finalize的兩個方法,ReRegisterForFinalize和SuppressFinalize。前者是請求系統完成對象的Finalize方法,後者是請求系統不要完成對象的Finalize方法。ReRegisterForFinalize方法其實就是將指向對象的指針從新添加到Finalization Queue中。這就出現了一個頗有趣的現象,由於在Finalization Queue中的對象能夠復生,若是在對象的Finalize方法中調用ReRegisterForFinalize方法,這樣就造成了一個在堆上永遠不會死去的對象,像鳳凰涅槃同樣每次死的時候均可以復生。性能

 

託管資源:優化

Net中的全部類型都是(直接或間接)從System.Object類型派生的。this

CTS中的類型被分紅兩大類——引用類型(reference type,又叫託管類型[managed type]),分配在內存堆上,值類型(value type)。值類型分配在堆棧上。如圖spa

alt

值類型在棧裏,先進後出,值類型變量的生命有前後順序,這個確保了值類型變量在推出做用域之前會釋放資源。比引用類型更簡單和高效。堆棧是從高地址往低地址分配內存。

引用類型分配在託管堆(Managed Heap)上,聲明一個變量在棧上保存,當使用new建立對象時,會把對象的地址存儲在這個變量裏。託管堆相反,從低地址往高地址分配內存,如圖

alt

.net中超過80%的資源都是託管資源。

 

非託管資源:

ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip ,文件句柄,GDI資源,數據庫鏈接等等資源。可能在使用的時候不少都沒有注意到!

 

.NET的GC機制有這樣兩個問題:

首先,GC並非能釋放全部的資源。它不能自動釋放非託管資源。

第二,GC並非實時性的,這將會形成系統性能上的瓶頸和不肯定性。

GC並非實時性的,這會形成系統性能上的瓶頸和不肯定性。因此有了IDisposable接口,IDisposable接口定義了Dispose方法,這個方法用來供程序員顯式調用以釋放非託管資源。使用using 語句能夠簡化資源管理。

示例

/// <summary>
/// 執行SQL語句,返回影響的記錄數
/// </summary>
/// <param name="SQLString">SQL語句</param>
/// <returns>影響的記錄數</returns>
public static int ExecuteSql(string SQLString)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
using (SqlCommand cmd = new SqlCommand(SQLString, connection))
{
try
{
connection.Open();
int rows = cmd.ExecuteNonQuery();
return rows;
}
catch (System.Data.SqlClient.SqlException e)
{
connection.Close();
throw e;
}
finally
{
cmd.Dispose();
connection.Close();
}
}
}
}

 

 

 

當你用Dispose方法釋放未託管對象的時候,應該調用GC.SuppressFinalize。若是對象正在終結隊列(finalization queue),GC.SuppressFinalize會阻止GC調用Finalize方法。由於Finalize方法的調用會犧牲部分性能。若是你的Dispose方法已經對委託管資源做了清理,就不必讓GC再調用對象的Finalize方法(MSDN)。附上MSDN的代碼,你們能夠參考.
 

public class BaseResource : IDisposable
{
// 指向外部非託管資源
private IntPtr handle;
// 此類使用的其它託管資源.
private Component Components;
// 跟蹤是否調用.Dispose方法,標識位,控制垃圾收集器的行爲
private bool disposed = false;

// 構造函數
public BaseResource()
{
// Insert appropriate constructor code here.
}

// 實現接口IDisposable.
// 不能聲明爲虛方法virtual.
// 子類不能重寫這個方法.
public void Dispose()
{
Dispose(true);
// 離開終結隊列Finalization queue
// 設置對象的阻止終結器代碼
//
GC.SuppressFinalize(this);
}

// Dispose(bool disposing) 執行分兩種不一樣的狀況.
// 若是disposing 等於 true, 方法已經被調用
// 或者間接被用戶代碼調用. 託管和非託管的代碼都能被釋放
// 若是disposing 等於false, 方法已經被終結器 finalizer 從內部調用過,
//你就不能在引用其餘對象,只有非託管資源能夠被釋放。
protected virtual void Dispose(bool disposing)
{
// 檢查Dispose 是否被調用過.
if (!this.disposed)
{
// 若是等於true, 釋放全部託管和非託管資源
if (disposing)
{
// 釋放託管資源.
Components.Dispose();
}
// 釋放非託管資源,若是disposing爲 false,
// 只會執行下面的代碼.
CloseHandle(handle);
handle = IntPtr.Zero;
// 注意這裏是非線程安全的.
// 在託管資源釋放之後能夠啓動其它線程銷燬對象,
// 可是在disposed標記設置爲true前
// 若是線程安全是必須的,客戶端必須實現。

}
disposed = true;
}
// 使用interop 調用方法
// 清除非託管資源.
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);

// 使用C# 析構函數來實現終結器代碼
// 這個只在Dispose方法沒被調用的前提下,才能調用執行。
// 若是你給基類終結的機會.
// 不要給子類提供析構函數.
~BaseResource()
{
// 不要重複建立清理的代碼.
// 基於可靠性和可維護性考慮,調用Dispose(false) 是最佳的方式
Dispose(false);
}

// 容許你屢次調用Dispose方法,
// 可是會拋出異常若是對象已經釋放。
// 不論你什麼時間處理對象都會覈查對象的是否釋放,
// check to see if it has been disposed.
public void DoSomething()
{
if (this.disposed)
{
throw new ObjectDisposedException();
}
}


// 不要設置方法爲virtual.
// 繼承類不容許重寫這個方法
public void Close()
{
// 無參數調用Dispose參數.
Dispose();
}

public static void Main()
{
// Insert code here to create
// and use a BaseResource object.
}
}

 

 

 

GC.Collect() 方法

做用:強制進行垃圾回收。

GC的方法:

名稱

說明

Collect()

強制對全部代進行即時垃圾回收。

Collect(Int32)

強制對零代到指定代進行即時垃圾回收。

Collect(Int32, GCCollectionMode)

強制在 GCCollectionMode 值所指定的時間對零代到指定代進行垃圾回收。


GC注意事項:

一、只管理內存,非託管資源,如文件句柄,GDI資源,數據庫鏈接等還須要用戶去管理

二、循環引用,網狀結構等的實現會變得簡單。GC的標誌也壓縮算法能有效的檢測這些關係,並將再也不被引用的網狀結構總體刪除。

三、GC經過從程序的根對象開始遍從來檢測一個對象是否可被其餘對象訪問,而不是用相似於COM中的引用計數方法。

四、GC在一個獨立的線程中運行來刪除再也不被引用的內存

五、GC每次運行時會壓縮託管堆

六、你必須對非託管資源的釋放負責。能夠經過在類型中定義Finalizer來保證資源獲得釋放。

七、對象的Finalizer被執行的時間是在對象再也不被引用後的某個不肯定的時間。注意並不是和C++中同樣在對象超出聲明週期時當即執行析構函數

八、Finalizer的使用有性能上的代價。須要Finalization的對象不會當即被清除,而須要先執行Finalizer.Finalizer不是在GC執行的線程被調用。GC把每個須要執行Finalizer的對象放到一個隊列中去,而後啓動另外一個線程來執行全部這些Finalizer.而GC線程繼續去刪除其餘待回收的對象。在下一個GC週期,這些執行完Finalizer的對象的內存纔會被回收。

九、.NET GC使用"代"(generations)的概念來優化性能。代幫助GC更迅速的識別那些最可能成爲垃圾的對象。在上次執行完垃圾回收後新建立的對象爲第0代對象。經歷了一次GC週期的對象爲第1代對象。經歷了兩次或更多的GC週期的對象爲第2代對象。代的做用是爲了區分局部變量和須要在應用程序生存週期中一直存活的對象。大部分第0代對象是局部變量。成員變量和全局變量很快變成第1代對象並最終成爲第2代對象。

十、GC對不一樣代的對象執行不一樣的檢查策略以優化性能。每一個GC週期都會檢查第0代對象。大約1/10的GC週期檢查第0代和第1代對象。大約1/100的GC週期檢查全部的對象。從新思考Finalization的代價:須要Finalization的對象可能比不須要Finalization在內存中停留額外9個GC週期。若是此時它尚未被Finalize,就變成第2代對象,從而在內存中停留更長時間。

相關文章
相關標籤/搜索