原文來自互聯網,由長沙DotNET技術社區編譯。 

.NET中的內存管理

資源分配

Microsoft .NET公共語言運行時要求從託管堆分配所有資源。當應用程序不再需要對象時,它們將自動釋放。

初始化進程後,運行時將保留地址空間的連續區域,該區域最初沒有爲其分配存儲空間。該地址空間區域是託管堆。堆還維護一個指針。該指針指示下一個對象將在堆中分配的位置。最初,將指針設置爲保留地址空間區域的基地址。

應用程序使用new運算符創建一個對象。該運算符首先確保新對象所需的字節適合保留區域(必要時進行存儲)。如果對象合適,則指針指向堆中的對象,調用該對象的構造函數,並且new運算符返回該對象的地址。

上圖顯示了一個由三個對象組成的託管堆:A,B和C。要分配的下一個對象將放置在NextObjPtr指向的位置(緊隨對象C之後)。

當應用程序調用new運算符創建對象時,該區域中可能沒有足夠的地址空間分配給該對象。堆通過將新對象的大小添加到NextObjPtr來檢測到這一點。如果NextObjPtr超出地址空間區域的末尾,則堆已滿,必須執行收集。

實際上,當第0代完全填滿時發生收集。簡而言之,生成是由垃圾收集器實現以提高性能的一種機制。這個想法是,新創建的對象是年輕一代的一部分,而在應用程序生命週期的早期創建的對象是老一代的對象。將對象分成幾代可以使垃圾收集器收集特定的世代,而不是收集託管堆中的所有對象。

垃圾收集算法

垃圾收集器檢查以查看堆中是否有不再由應用程序使用的對象。如果存在此類對象,則可以回收這些對象使用的內存。(如果沒有更多的內存可用於堆,則new運算符將引發OutOfMemoryException。)

每個應用程序都有一組根。根標識存儲位置,這些存儲位置引用託管堆上的對象或設置爲null的對象。例如,應用程序中的所有全局和靜態對象指針都被視爲應用程序根目錄的一部分。另外,線程堆棧上的任何局部變量/參數對象指針都被視爲應用程序根目錄的一部分。最後,任何包含指向託管堆中對象的指針的CPU寄存器也被視爲應用程序根目錄的一部分。活動根的列表由即時(JIT)編譯器和公共語言運行時維護,並且可以由垃圾收集器的算法訪問。

當垃圾收集器開始運行時,它假定堆中的所有對象都是垃圾。換句話說,它假定應用程序的任何根都沒有引用堆中的任何對象。現在,垃圾收集器開始遍歷根目錄,併爲從根目錄可訪問的所有對象建立圖形。例如,垃圾收集器可以定位一個指向堆中對象的全局變量。

下圖顯示了具有幾個已分配對象的堆,其中應用程序的根直接引用對象A,C,D和F。所有這些對象都成爲圖形的一部分。在添加對象D時,收集器會注意到該對象引用了對象H,並且對象H也已添加到圖中。收集器將繼續遞歸遍歷所有可到達的對象。

圖的這一部分完成後,垃圾收集器將檢查下一個根並再次遍歷對象。當垃圾收集器從一個對象移動到另一個對象時,如果它試圖將一個對象添加到先前添加的圖形中,則垃圾收集器可以停止沿該路徑移動。這有兩個目的。首先,它不會多次遍歷一組對象,因此可以顯着提高性能。其次,如果您有任何循環鏈接的對象列表,它可以防止無限循環。

一旦檢查完所有的根,垃圾收集器的圖形就會包含從應用程序的根以某種方式可以訪問的所有對象的集合。應用程序無法訪問該圖中未包含的任何對象,因此將其視爲垃圾。

垃圾收集器現在線性地遍歷堆,尋找垃圾對象的連續塊(現在被認爲是可用空間)。然後,垃圾收集器將非垃圾對象向下移動到內存中(使用標準的memcpy函數),從而消除了堆中的所有間隙。當然,在內存中移動對象會使指向該對象的所有指針無效。因此,垃圾收集器必須修改應用程序的根,以便指針指向對象的新位置。另外,如果任何對象包含指向另一個對象的指針,則垃圾回收器還負責更正這些指針。

下圖顯示了收集後的託管堆。

在識別完所有垃圾之後,所有非垃圾都已壓縮,所有非垃圾指針都已固定,NextObjPtr定位在最後一個非垃圾對象之後。此時,再次嘗試新操作,併成功創建應用程序請求的資源。

GC會對性能產生重大影響,這是使用託管堆的主要缺點。但是,請記住,GC僅在堆已滿時才發生,並且在此之前,託管堆要比C運行時堆快得多。運行時的垃圾收集器還使用Generations提供了一些優化,可以大大提高垃圾收集的性能。

您不再需要實現管理應用程序使用的任何資源的生存期的任何代碼。現在,不可能泄漏資源,因爲可以在某個時候收集從應用程序的根目錄無法訪問的任何資源。此外,也無法訪問已釋放的資源,因爲如果可訪問資源將不會被釋放。如果無法訪問,則您的應用程序無法訪問它。

以下代碼演示瞭如何分配和管理資源:

class Application

{

public static int Main(String[] args)

{

// ArrayList object created in heap, myArray is now in root

ArrayList myArray = new ArrayList();

// Create 10000 objects in the heap

for (int x = 0; x < 10000; x++)

{

myArray.Add(new Object()); // Object object created in heap

}

// Right now, myArray is a root (on the thread's stack). So,

// myArray is reachable and the 10000 objects it points to are also reachable.

Console.WriteLine(myArray.Count);

// After the last reference to myArray in the code, myArray is not a root.

// Note that the method doesn't have to return, the JIT compiler knows

// to make myArray not a root after the last reference to it in the code.

// Since myArray is not a root, all 10001 objects are not reachable

// and are considered garbage. However, the objects are not

// collected until a GC is performed.

}

}

如果GC非常出色,那麼您可能想知道爲什麼它不在ANSI C ++中。 原因是垃圾收集器必須能夠標識應用程序的根,還必須能夠找到所有對象指針。 C ++的問題在於它允許將指針從一種類型轉換爲另一種類型,並且無法知道指針所指的是什麼。 在公共語言運行庫中,託管堆始終知道對象的實際類型,並且元數據信息用於確定對象的哪些成員引用其他對象。

世代

純粹爲了提高性能而存在的垃圾收集器的一個功能稱爲“世代”。分代垃圾收集器(也稱爲臨時垃圾收集器)進行以下假設:

對象越新,其生存期就會越短。 對象越舊,其壽命將越長。 較新的對象往往彼此之間具有很強的關係,並且經常在同一時間訪問。 壓縮一部分堆比壓縮整個堆要快。

初始化後,託管堆不包含任何對象。如下圖所示,添加到堆中的對象被稱爲第0代。簡而言之,第0代中的對象是從未被垃圾收集器檢查過的年輕對象。

Memory6.gif

現在,如果將更多對象添加到堆中,則將填充堆,並且必須進行垃圾回收。垃圾收集器分析堆時,將構建垃圾(此處以綠色顯示)和非垃圾對象的圖形。可以將收集到的所有對象壓縮到堆的最左側。這些對象在收藏中倖存下來,並且更舊,現在被認爲是第一代。

隨着更多對象添加到堆中,這些新的年輕對象將放置在第0代中。如果再次填充第0代,則會執行GC。這次,將第1代中倖存的所有對象壓縮並視爲第2代(請參見下圖)。現在壓縮了第0代中的所有幸存者,並認爲它們是第1代。第0代當前不包含任何對象,但是所有新對象將進入第0代。

當前,第二代是運行時的垃圾收集器支持的最高一代。當將來發生收集時,當前第2代中尚存的所有對象僅保留在第2代中。

世代GC性能優化

分代垃圾收集提高了性能。當堆填滿併發生收集時,垃圾收集器可以選擇僅檢查第0代中的對象,而忽略任何更大的後代中的對象。畢竟,對象越新,則預期壽命越短。因此,收集和壓縮第0代對象很可能會從堆中回收大量空間,並且比收集器檢查所有代的對象要快。

分代收集器可以通過不遍歷託管堆中的每個對象來提供更多優化。如果根或對象引用的是舊對象,則垃圾收集器可以忽略任何較舊對象的內部引用,從而減少了構建可訪問對象圖所需的時間。當然,舊對象可能是指新對象。爲了檢查這些對象,收集器可以利用系統的寫監視支持(由Kernel32.dll中的Win32 GetWriteWatch函數提供)。此支持使收集器知道自上次收集以來已將哪些舊對象(如果有)寫入了。可以檢查這些特定的舊對象的引用,以查看它們是否引用了任何新對象。

如果收集第0代未提供必要的存儲量,則收集器可以嘗試收集第1代和第0代的對象。如果所有其他操作均失敗,則收集器可以收集第2代,第1代和第9代的所有對象。0。

前面提到的一種假設是,較新的對象之間往往具有很強的關係,並且經常在同一時間訪問。由於新對象是在內存中連續分配的,因此您可以從引用的位置獲得性能。更具體地說,很可能所有對象都可以駐留在CPU的緩存中。您的應用程序將以驚人的速度訪問這些對象,因爲CPU將能夠執行其大多數操作,而不會導致強制RAM訪問的高速緩存未命中。

微軟的性能測試表明,託管堆分配比Win32 HeapAlloc函數執行的標準分配更快。這些測試還表明,在200 MHz Pentium上執行第0代完整GC所需的時間少於1毫秒。Microsoft的目標是使GC花費的時間不比普通頁面錯誤多。

Win32堆的缺點:

大多數堆(例如C運行時堆)在找到可用空間的任何地方分配對象。因此,如果我連續創建多個對象,則這些對象很有可能將被兆字節的地址空間分隔開。但是,在託管堆中,連續分配幾個對象可確保對象在內存中是連續的。 從Win32堆分配內存時,必須檢查該堆以找到可以滿足請求的內存塊。這在託管堆中不是必需的,因爲此處對象在內存中是連續的。 在Win32堆中,必須維護堆維護的數據結構。另一方面,託管堆僅需要增加堆指針。

終接器

垃圾收集器提供了您可能想利用的其他功能:終結處理。最終確定允許資源在被收集後對其進行適當的清理。通過使用終結處理,當垃圾回收器決定釋放資源的內存時,代表文件或網絡連接的資源便能夠正確清理自身。

當垃圾收集器檢測到對象是垃圾時,垃圾收集器將調用對象的Finalize方法(如果存在),然後回收該對象的內存。例如,假設您具有以下類型(在C#中):

public class BaseObj

{

public BaseObj()

{

}

protected override void Finalize()

{

// Perform resource cleanup code here

// Example: Close file/Close network connection

Console.WriteLine("In Finalize.");

}

}

現在,您可以通過調用以下內容來創建該對象的實例:

BaseObj bo = new BaseObj();

將來的某個時候,垃圾收集器將確定該對象爲垃圾。發生這種情況時,垃圾收集器將看到該類型具有Finalize方法,並將調用該方法,從而使“ In Finalize”出現在控制檯窗口中並回收該對象使用的內存塊。

許多習慣於使用C ++進行編程的開發人員都會在析構函數和Finalize方法之間建立直接的關聯。但是,對象終結處理和析構函數具有非常不同的語義,在考慮終結處理時,最好忘記您對析構函數的瞭解。受管對象永遠不會有析構函數。

設計類型時,最好避免使用Finalize方法。有幾個原因:

可終結對象被提升爲較早的一代,這增加了內存壓力,並在垃圾收集器確定對象爲垃圾時阻止了對象的內存被收集。此外,該對象直接或間接引用的所有對象也將得到提升。

可終結對象需要更長的分配時間。

強制垃圾收集器執行Finalize方法會嚴重影響性能。請記住,每個對象都已完成。因此,如果我有10,000個對象的數組,則每個對象都必須調用其Finalize方法。

終結對象可以引用其他(不可終結)對象,從而不必要地延長其壽命。實際上,您可能需要考慮將類型分爲兩種不同的類型:一種輕型類型,其具有不引用任何其他對象的Finalize方法,一個單獨的類型,其類型不具有引用其他對象的Finalize方法。

您無法控制Finalize方法何時執行。該對象可能會保留資源,直到下一次垃圾收集器運行爲止。

當應用程序終止時,某些對象仍然可以訪問,並且不會調用其Finalize方法。如果後臺線程正在使用對象,或者在應用程序關閉或AppDomain卸載期間創建了對象,則會發生這種情況。此外,默認情況下,應用程序退出時,不可達對象不會調用Finalize方法,因此應用程序可能會迅速終止。當然,將回收所有操作系統資源,但是託管堆中的任何對象都無法正常清理。您可以通過調用System.GC類型的RequestFinalizeOnShutdown方法來更改此默認行爲。但是,應謹慎使用此方法,因爲調用它意味着您的類型正在控制整個應用程序的策略。

運行時無法保證Finalize方法的調用順序。例如,假設有一個對象包含一個指向內部對象的指針。垃圾收集器檢測到兩個對象都是垃圾。此外,假設首先調用內部對象的Finalize方法。現在,允許外部對象的Finalize方法訪問內部對象並對其調用方法,但是內部對象已完成,並且結果可能無法預測。因此,強烈建議Finalize方法不要訪問任何內部成員對象。

如果確定類型必須實現Finalize方法,則請確保代碼儘快執行。避免所有會阻止Finalize方法的操作,包括任何線程同步操作。另外,如果您讓任何異常轉義了Finalize方法,則系統僅假定Finalize方法已返回,並繼續調用其他對象的Finalize方法。

當編譯器爲構造函數生成代碼時,編譯器會自動插入對基本類型的構造函數的調用。同樣,當C ++編譯器爲析構函數生成代碼時,編譯器會自動插入對基本類型的析構函數的調用。終結方法不同於析構函數。編譯器對Finalize方法沒有特殊知識,因此編譯器不會自動生成代碼以調用基本類型的Finalize方法。如果您想要這種行爲,並且經常這樣做,那麼必須從類型的Finalize方法中顯式調用基本類型的Finalize方法:

 

public class BaseObj

{

public BaseObj()

{

}

protected override void Finalize()

{

Console.WriteLine("In Finalize.");

base.Finalize(); // Call base type's Finalize

}

}

請注意,通常將基類型的Finalize方法稱爲派生類型的Finalize方法中的最後一條語句。這樣可以使基礎對象保持儘可能長的生命。由於調用基本類型的Finalize方法很常見,因此C#的語法簡化了您的工作。在C#中,以下代碼:

class MyObject { MyObject() { } }

終結內部

當應用程序創建新對象時,新運算符將從堆中分配內存。如果對象的類型包含Finalize方法,則將指向該對象的指針放在終結隊列中。終結隊列是由垃圾收集器控制的內部數據結構。隊列中的每個條目都指向一個對象,在可以回收該對象的內存之前,應調用該對象的Finalize方法。

下圖顯示了包含多個對象的堆。從應用程序的根目錄可以訪問其中的某些對象,而某些則不能。創建對象C,E,F,I和J時,系統檢測到這些對象具有Finalize方法,並將指向這些對象的指針添加到了終結隊列中。

發生GC時,對象B,E,G,H,I和J被確定爲垃圾。垃圾收集器掃描完成隊列,以查找指向這些對象的指針。當找到一個指針時,該指針將從終結隊列中刪除,並附加到易碎隊列(發音爲“ F-reachable”)。易碎隊列是由垃圾收集器控制的另一個內部數據結構。易碎隊列中的每個指針都標識一個對象,該對象已準備好調用其Finalize方法。

收集之後,託管堆如下圖所示。在這裏,您看到對象B,G和H佔用的內存已被回收,因爲這些對象沒有需要調用的Finalize方法。但是,無法回收對象E,I和J佔用的內存,因爲尚未調用它們的Finalize方法。

有一個專用的運行時線程專用於調用Finalize方法。當可訪問隊列爲空時(通常是這種情況),該線程進入睡眠狀態。但是,當出現條目時,該線程將喚醒,從隊列中刪除每個條目,並調用每個對象的Finalize方法。因此,您不應在Finalize方法中執行任何有關執行代碼的線程的假設的代碼。例如,避免在Finalize方法中訪問線程本地存儲。

終結隊列與易碎隊列的交互非常有趣。首先,讓我告訴您易碎隊列的名稱。f很明顯,代表定稿;易碎隊列中的每個條目都應調用其Finalize方法。名稱的“可到達”部分表示對象可到達。換句話說,易碎隊列被視爲根,就像全局變量和靜態變量是根一樣。因此,如果對象在易碎隊列中,則該對象可訪問且不是垃圾。

簡而言之,當對象不可訪問時,垃圾收集器將其視爲對象垃圾。然後,當垃圾收集器將對象的條目從終結隊列移到可訪問隊列時,該對象不再被視爲垃圾,並且不回收其內存。至此,垃圾收集器已經完成了對垃圾的識別。某些標識爲垃圾的對象已被重新分類爲非垃圾。垃圾收集器壓縮可回收內存,特殊的運行時線程清空易碎隊列,執行每個對象的Finalize方法。

下次調用垃圾回收器時,它會看到最終對象是真正的垃圾,因爲應用程序的根不指向該對象,並且易碎隊列不再指向該對象。現在,只需回收該對象的內存即可。這裏要了解的重要一點是,需要兩個GC來回收需要終結處理的對象使用的內存。實際上,可能需要兩個以上的集合,因爲這些對象可以提升爲較老的一代。上圖顯示了第二個GC之後託管堆的外觀。

處置方法

使用此方法可以關閉或釋放由實現此接口的類的實例持有的非託管資源,例如文件,流和句柄。按照慣例,此方法用於與釋放對象擁有的資源或準備對象重用相關的所有任務。

在實現此方法時,對象必須設法通過在包含層次結構中傳播調用來確保釋放所有保留的資源。例如,如果對象A分配了對象B,而對象B分配了對象C,則A的Dispose實現必須調用B上的Dispose,後者又必須調用C上的Dispose。對象還必須調用其基類的Dispose方法。如果基類實現IDisposable。

如果多次調用對象的 Dispose方法,則該對象必須忽略第一個調用之後的所有調用。如果多次調用其Dispose方法,則該對象不得引發異常。如果由於已釋放資源並且以前未調用過Dispose而發生錯誤,則Dispose可能引發異常。

因爲必須顯式調用 Dispose方法,所以實現IDisposable的對象還必須實現終結器,以在不調用Dispose時處理釋放資源。默認情況下,垃圾回收器將在回收對象的內存之前自動調用其終結器。但是,一旦調用了Dispose方法,垃圾收集器通常就不需要調用已處理對象的終結器。爲了防止自動完成,Dispose實現可以調用GC.SuppressFinalize方法。

通過System.GC直接控制

System.GC類型使您的應用程序可以直接控制垃圾收集器。您可以通過讀取GC.MaxGeneration屬性來查詢託管堆支持的最大生成量。當前,GC.MaxGeneration屬性始終返回2。

也可以通過調用此處顯示的兩個方法之一來強制垃圾收集器執行收集:

void GC.Collect(Int32 Generation)

void GC.Collect()

第一種方法允許您指定要收集的世代。您可以將0範圍內的任何整數傳遞給GC.MaxGeneration(含)。傳遞0導致生成0被收集;傳遞1導致收集第1代和第0代;傳遞2會導致生成2、1、0和0。不帶參數的Collect方法的版本強制所有世代的完整集合,等效於調用:

GC.Collect(GC.MaxGeneration);

GC類型還提供了WaitForPendingFinalizers方法。此方法只是掛起調用線程,直到處理易碎隊列的線程清空了隊列,然後調用每個對象的Finalize方法。在大多數應用程序中,您不太可能需要調用此方法。

最後,垃圾收集器提供了兩種方法,可讓您確定對象當前處於哪個世代:

Int32 GetGeneration(Object obj) Int32 GetGeneration(WeakReference wr)

GetGeneration的第一個版本將對象引用作爲參數,而第二個版本將WeakReference引用作爲參數。當然,返回的值將介於0到GC.MaxGeneration之間(含)。

相關文章