GC的前世與今生
雖然本文是以.NET作為目标來講述GC,但是GC的概念并非才誕生不久。早在1958年,由鼎鼎大名的圖林獎得主John McCarthy所實作的Lisp語言就已經提供了GC的功能,這是GC的第一次出現。Lisp的程式員認為記憶體管理太重要了,是以不能由程式員自己來管理。
但後來的日子裡Lisp卻沒有成氣候,采用記憶體手動管理的語言占據了上風,以C為代表。出于同樣的理由,不同的人卻又不同的看法,C程式員認為記憶體管理太重要了,是以不能由系統來管理,并且譏笑Lisp程式慢如烏龜的運作速度。的确,在那個對每一個Byte都要精心計算的年代GC的速度和對系統資源的大量占用使很多人的無法接受。而後,1984年由Dave Ungar開發的Smalltalk語言第一次采用了Generational garbage collection的技術(這個技術在下文中會談到),但是Smalltalk也沒有得到十分廣泛的應用。
直到20世紀90年代中期GC才以主角的身份登上了曆史的舞台,這不得不歸功于Java的進步,今日的GC已非吳下阿蒙。Java采用VM(Virtual Machine)機制,由VM來管理程式的運作當然也包括對GC管理。90年代末期.NET出現了,.NET采用了和Java類似的方法由CLR(Common Language Runtime)來管理。這兩大陣營的出現将人們引入了以虛拟平台為基礎的開發時代,GC也在這個時候越來越得到大衆的關注。
為什麼要使用GC呢?也可以說是為什麼要使用記憶體自動管理?有下面的幾個原因:
1、提高了軟體開發的抽象度;
2、程式員可以将精力集中在實際的問題上而不用分心來管理記憶體的問題;
3、可以使子產品的接口更加的清晰,減小子產品間的偶合;
4、大大減少了記憶體人為管理不當所帶來的Bug;
5、使記憶體管理更加高效。
總的說來就是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算法。
一、Mark-Compact 标記壓縮算法
簡單地把.NET的GC算法看作Mark-Compact算法。階段1: Mark-Sweep 标記清除階段,先假設heap中所有對象都可以回收,然後找出不能回收的對象,給這些對象打上标記,最後heap中沒有打标記的對象都是可以被回收的;階段2: Compact 壓縮階段,對象回收之後heap記憶體空間變得不連續,在heap中移動這些對象,使他們重新從heap基位址開始連續排列,類似于磁盤空間的碎片整理。
Heap記憶體經過回收、壓縮之後,可以繼續采用前面的heap記憶體配置設定方法,即僅用一個指針記錄heap配置設定的起始位址就可以。主要處理步驟:将線程挂起→确定roots→建立reachable objects graph→對象回收→heap壓縮→指針修複。可以這樣了解roots:heap中對象的引用關系錯綜複雜(交叉引用、循環引用),形成複雜的graph,roots是CLR在heap之外可以找到的各種入口點。
GC搜尋roots的地方包括全局對象、靜态變量、局部對象、函數調用參數、目前CPU寄存器中的對象指針(還有finalization queue)等。主要可以歸為2種類型:已經初始化了的靜态變量、線程仍在使用的對象(stack+CPU register) 。 Reachable objects:指根據對象引用關系,從roots出發可以到達的對象。例如目前執行函數的局部變量對象A是一個root object,他的成員變量引用了對象B,則B是一個reachable object。從roots出發可以建立reachable objects graph,剩餘對象即為unreachable,可以被回收 。
指針修複是因為compact過程移動了heap對象,對象位址發生變化,需要修複所有引用指針,包括stack、CPU register中的指針以及heap中其他對象的引用指針。Debug和release執行模式之間稍有差別,release模式下後續代碼沒有引用的對象是unreachable的,而debug模式下需要等到目前函數執行完畢,這些對象才會成為unreachable,目的是為了調試時跟蹤局部對象的内容。傳給了COM+的托管對象也會成為root,并且具有一個引用計數器以相容COM+的記憶體管理機制,引用計數器為0時,這些對象才可能成為被回收對象。Pinned objects指配置設定之後不能移動位置的對象,例如傳遞給非托管代碼的對象(或者使用了fixed關鍵字),GC在指針修複時無法修改非托管代碼中的引用指針,是以将這些對象移動将發生異常。pinned objects會導緻heap出現碎片,但大部分情況來說傳給非托管代碼的對象應當在GC時能夠被回收掉。
二、 Generational 分代算法
程式可能使用幾百M、幾G的記憶體,對這樣的記憶體區域進行GC操作成本很高,分代算法具備一定統計學基礎,對GC的性能改善效果比較明顯。将對象按照生命周期分成新的、老的,根據統計分布規律所反映的結果,可以對新、老區域采用不同的回收政策和算法,加強對新區域的回收處理力度,争取在較短時間間隔、較小的記憶體區域内,以較低成本将執行路徑上大量新近抛棄不再使用的局部對象及時回收掉。分代算法的假設前提條件:
1、大量新建立的對象生命周期都比較短,而較老的對象生命周期會更長;
2、對部分記憶體進行回收比基于全部記憶體的回收操作要快;
3、新建立的對象之間關聯程度通常較強。heap配置設定的對象是連續的,關聯度較強有利于提高CPU cache的命中率,.NET将heap分成3個代齡區域: Gen 0、Gen 1、Gen 2;
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稱為full GC,通常成本很高。粗略的計算0代和1代GC應當能在幾毫秒到幾十毫秒之間完成,Gen 2 heap比較大時,full GC可能需要花費幾秒時間。大緻上來講.NET應用運作期間,2代、1代和0代GC的頻率應當大緻為1:10:100。
三、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類型派生的。
CTS中的類型被分成兩大類——引用類型(reference type,又叫托管類型[managed type]),配置設定在記憶體堆上;值類型(value type),配置設定在堆棧上。如圖:
值類型在棧裡,先進後出,值類型變量的生命有先後順序,這個確定了值類型變量在退出作用域以前會釋放資源。比引用類型更簡單和高效。堆棧是從高位址往低位址配置設定記憶體。
引用類型配置設定在托管堆(Managed Heap)上,聲明一個變量在棧上儲存,當使用new建立對象時,會把對象的位址存儲在這個變量裡。托管堆相反,從低位址往高位址配置設定記憶體,如圖:
.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
publicstaticint 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的代碼,大家可以參考。
publicclass BaseResource : IDisposable
{
// 指向外部非托管資源
private IntPtr handle;
// 此類使用的其它托管資源.
private Component Components;
// 跟蹤是否調用.Dispose方法,辨別位,控制垃圾收集器的行為
privatebool disposed =false;
// 構造函數
public BaseResource()
{
// Insert appropriate constructor code here.
}
// 實作接口IDisposable.
// 不能聲明為虛方法virtual.
// 子類不能重寫這個方法.
publicvoid Dispose()
{
Dispose(true);
// 離開終結隊列Finalization queue
// 設定對象的阻止終結器代碼
//
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) 執行分兩種不同的情況.
// 如果disposing 等于 true, 方法已經被調用
// 或者間接被使用者代碼調用. 托管和非托管的代碼都能被釋放
// 如果disposing 等于false, 方法已經被終結器 finalizer 從内部調用過,
//你就不能在引用其他對象,隻有非托管資源可以被釋放。
protectedvirtualvoid 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")]
privateexternstatic Boolean CloseHandle(IntPtr handle);
// 使用C# 析構函數來實作終結器代碼
// 這個隻在Dispose方法沒被調用的前提下,才能調用執行。
// 如果你給基類終結的機會.
// 不要給子類提供析構函數.
~BaseResource()
{
// 不要重複建立清理的代碼.
// 基于可靠性和可維護性考慮,調用Dispose(false) 是最佳的方式
Dispose(false);
}
// 允許你多次調用Dispose方法,
// 但是會抛出異常如果對象已經釋放。
// 不論你什麼時間處理對象都會核查對象的是否釋放,
// check to see if it has been disposed.
publicvoid DoSomething()
{
if (this.disposed)
{
thrownew ObjectDisposedException();
}
}
// 不要設定方法為virtual.
// 繼承類不允許重寫這個方法
publicvoid Close()
{
// 無參數調用Dispose參數.
Dispose();
}
publicstaticvoid Main()
{
// Insert code here to create
// and use a BaseResource object.
}
}
GC.Collect() 方法
作用:強制進行垃圾回收。
GC的方法:
名稱 | 說明 |
Collect() | 強制對所有代進行即時垃圾回收。 |
Collect(Int32) | 強制對零代到指定代進行即時垃圾回收。 |
Collect(Int32, GCCollectionMode) | 強制在 GCCollectionMode 值所指定的時間對零代到指定代進行垃圾回收 |
GC注意事項:
1、隻管理記憶體,非托管資源,如檔案句柄,GDI資源,資料庫連接配接等還需要使用者去管理。
2、循環引用,網狀結構等的實作會變得簡單。GC的标志-壓縮算法能有效的檢測這些關系,并将不再被引用的網狀結構整體删除。
3、GC通過從程式的根對象開始周遊來檢測一個對象是否可被其他對象通路,而不是用類似于COM中的引用計數方法。
4、GC在一個獨立的線程中運作來删除不再被引用的記憶體。
5、GC每次運作時會壓縮托管堆。
6、你必須對非托管資源的釋放負責。可以通過在類型中定義Finalizer來保證資源得到釋放。
7、對象的Finalizer被執行的時間是在對象不再被引用後的某個不确定的時間。注意并非和C++中一樣在對象超出聲明周期時立即執行析構函數
8、Finalizer的使用有性能上的代價。需要Finalization的對象不會立即被清除,而需要先執行Finalizer.Finalizer,不是在GC執行的線程被調用。GC把每一個需要執行Finalizer的對象放到一個隊列中去,然後啟動另一個線程來執行所有這些Finalizer,而GC線程繼續去删除其他待回收的對象。在下一個GC周期,這些執行完Finalizer的對象的記憶體才會被回收。