.NET中的内存管理
原文來自互聯網,由長沙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之間(含)。
總結
以上是生活随笔為你收集整理的.NET中的内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WebAssembly增加Go语言绑定
- 下一篇: .Net微服务实战之技术架构分层篇