在 .NET 的所有技術中,最具爭議的恐怕是垃圾收集 (Garbage Collection , GC) 了。作為 .NET 框架中一個重要的部分,托管堆和垃圾收集機制對我們中的大" name="description" />
MILY: 宋體; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">在.NET的所有技術中,最具爭議的恐怕是垃圾收集(Garbage Collection,GC)了。作為.NET框架中一個重要的部分,托管堆和垃圾收集機制對我們中的大部分人來說是陌生的概念。在這篇文章中將要討論托管堆,和你將從中得到怎樣的好處。
為什么要托管堆?
.NET框架包含一個托管堆,所有的.NET語言在分配引用類型對象時都要使用它。像值類型這樣的輕量級對象始終分配在棧中,但是所有的類實例和數組都被生成在一個內存池中,這個內存池就是托管堆。
垃圾收集器的基本算法很簡單:
● 將所有的托管內存標記為垃圾
● 尋找正被使用的內存塊,并將他們標記為有效
● 釋放所有沒有被使用的內存塊
● 整理堆以減少碎片
托管堆優化
看上去似乎很簡單,但是垃圾收集器實際采用的步驟和堆管理系統的其他部分并非微不足道,其中常常涉及為提高性能而作的優化設計。舉例來說,垃圾收集遍歷整個內存池具有很高的開銷。然而,研究表明大部分在托管堆上分配的對象只有很短的生存期,因此堆被分成三個段,稱作generations。新分配的對象被放在generation 0中。這個generation是最先被回收的——在這個generation中最有可能找到不再使用的內存,由于它的尺寸很?。ㄐ〉阶阋苑胚M處理器的L2 cache中),因此在它里面的回收將是最快和最高效的。
托管堆的另外一種優化操作與locality of reference規則有關。該規則表明,一起分配的對象經常被一起使用。如果對象們在堆中位置很緊湊的話,高速緩存的性能將會得到提高。由于托管堆的天性,對象們總是被分配在連續的地址上,托管堆總是保持緊湊,結果使得對象們始終彼此靠近,永遠不會分得很遠。這一點與標準堆提供的非托管代碼形成了鮮明的對比,在標準堆中,堆很容易變成碎片,而且一起分配的對象經常分得很遠。
還有一種優化是與大對象有關的。通常,大對象具有很長的生存期。當一個大對象在.NET托管堆中產生時,它被分配在堆的一個特殊部分中,這部分堆永遠不會被整理。因為移動大對象所帶來的開銷超過了整理這部分堆所能提高的性能。
關于外部資源(External Resources)的問題
垃圾收集器能夠有效地管理從托管堆中釋放的資源,但是資源回收操作只有在內存緊張而觸發一個回收動作時才執行。那么,類是怎樣來管理像數據庫連接或者窗口句柄這樣有限的資源的呢?等待,直到垃圾回收被觸發之后再清理數據庫連接或者文件句柄并不是一個好方法,這會嚴重降低系統的性能。
所有擁有外部資源的類,在這些資源已經不再用到的時候,都應當執行Close或者Dispose方法。從Beta2(譯注:本文中所有的Beta2均是指.NET Framework Beta2,不再特別注明)開始,Dispose模式通過IDisposable接口來實現。這將在本文的后續部分討論。
需要清理外部資源的類還應當實現一個終止操作(finalizer)。在C#中,創建終止操作的首選方式是在析構函數中實現,而在Framework層,終止操作的實現則是通過重載System.Object.Finalize 方法。以下兩種實現終止操作的方法是等效的:
~OverdueBookLocator()
{
Dispose(false);
}
和:
public void Finalize()
{
base.Finalize();
Dispose(false);
}
在C#中,同時在Finalize方法和析構函數實現終止操作將會導致錯誤的產生。
除非你有足夠的理由,否則你不應該創建析構函數或者Finalize方法。終止操作會降低系統的性能,并且增加執行期的內存開銷。同時,由于終止操作被執行的方式,你并不能保證何時一個終止操作會被執行。
內存分配和垃圾回收的細節
對GC有了一個總體印象之后,讓我們來討論關于托管堆中的分配與回收工作的細節。托管堆看起來與我們已經熟悉的C++編程中的傳統的堆一點都不像。在傳統的堆中,數據結構習慣于使用大塊的空閑內存。在其中查找特定大小的內存塊是一件很耗時的工作,尤其是當內存中充滿碎片的時候。與此不同,在托管堆中,內存被組制成連續的數組,指針總是巡著已經被使用的內存和未被使用的內存之間的邊界移動。當內存被分配的時候,指針只是簡單地遞增——由此而來的一個好處是,分配操作的效率得到了很大的提升。
當對象被分配的時候,它們一開始被放在generation 0中。當generation 0的大小快要達到它的上限的時候,一個只在generation 0中執行的回收操作被觸發。由于generation 0的大小很小,因此這將是一個非??斓?/SPAN>GC過程。這個GC過程的結果是將generation 0徹底的刷新了一遍。不再使用的對象被釋放,確實正被使用的對象被整理并移入generation 1中。
當generation 1的大小隨著從generation 0中移入的對象數量的增加而接近它的上限的時候,一個回收動作被觸發來在generation 0和generation 1中執行GC過程。如同在generation 0中一樣,不再使用的對象被釋放,正在被使用的對象被整理并移入下一個generation中。大部分GC過程的主要目標是generation 0,因為在generation 0中最有可能存在大量的已不再使用的臨時對象。對generation 2的回收過程具有很高的開銷,并且此過程只有在generation 0和generation 1的GC過程不能釋放足夠的內存時才會被觸發。如果對generation 2的GC過程仍然不能釋放足夠的內存,那么系統就會拋出OutOfMemoryException異常
帶有終止操作的對象的垃圾收集過程要稍微復雜一些。當一個帶有終止操作的對象被標記為垃圾時,它并不會被立即釋放。相反,它會被放置在一個終止隊列(finalization queue)中,此隊列為這個對象建立一個引用,來避免這個對象被回收。后臺線程為隊列中的每個對象執行它們各自的終止操作,并且將已經執行過終止操作的對象從終止隊列中刪除。只有那些已經執行過終止操作的對象才會在下一次垃圾回收過程中被從內存中刪除。這樣做的一個后果是,等待被終止的對象有可能在它被清除之前,被移入更高一級的generation中,從而增加它被清除的延遲時間。
需要執行終止操作的對象應當實現IDisposable接口,以便客戶程序通過此接口快速執行終止動作。IDisposable接口包含一個方法——Dispose。這個被Beta2引入的接口,采用一種在Beta2之前就已經被廣泛使用的模式實現。從本質上講,一個需要終止操作的對象暴露出Dispose方法。這個方法被用來釋放外部資源并抑制終止操作,就象下面這個程序片斷所演示的那樣:
public class OverdueBookLocator: IDisposable
{
~OverdueBookLocator()
{
InternalDispose(false);
}
public void Dispose()
{
InternalDispose(true);
}
protected void InternalDispose(bool disposing)
{
if(disposing)
{
GC.SuppressFinalize(this);
// Dispose of managed objects if disposing.
}
// free external resources here
}
原文轉自:http://www.anti-gravitydesign.com