C# 自動內存管理

一、垃圾回收

VES內置了垃圾回收支持,垃圾回收器只負責內存管理,它沒有提供一個自動的系統來管理與內存無關的資源。
.NET的垃圾回收
多數CLI實現使用一個分代的(generational)、支持壓縮的(compacting)、以及基於mark-and-sweep(標記並清除)的算法回收內存。
分代的:是因爲只存活過短暫時間的對象與已經在垃圾回收時活下來的對象(原因是對象仍在使用)相比,前者會被更早地清理掉,這一點符合內存分配的常規模式:已經存活過較長時間的對象,會比最近才實例化的對象存活得更久一些。
.NET垃圾回收器使用了mark-and-sweep算法。在每次執行垃圾回收期間,它都會標記出將要回收的對象,並將剩餘的對象壓縮到一起,確保他們之間沒有“髒”空間。使用壓縮機制來填充由回收的對象騰出來的空間,通常會使新對象能以更快的速度實例化,這是因爲不必搜索內存爲一次新的分配尋找空間。
垃圾回收器會考慮機器資源以及執行時對那些資源的需求,例如,如果計算機內存尚餘大量空間,垃圾回收器就很好運行,並很少花時間去清理那些資源。

二、回收過程細節

什麼是垃圾,如何識別:
第一步:在一次垃圾回收週期開始的時候,它識別對象的所有根引用。根引用是來自靜態變量、CPU寄存器以及局部變量或者參數實例的任何引用。基於這個根引用列表,垃圾回收可以遍歷每個根引用標識的樹形結構,並遞歸確定所有根引用指向的對象。這樣,垃圾回收器就可以創建一個所有可達對象的圖。

第二步:執行垃圾時,垃圾回收不是枚舉所有訪問不到的對象,相反,它是把所有可達對象(第一步完成的可達對象圖)壓縮到一起,從而覆蓋不可訪問的對象(也就是垃圾)所佔用的內存。

第三步:爲了定位和移動所有可達對象,系統要在垃圾回收器運行期間維持狀態的一致性。爲此,進程中所有的託管線程都會在垃圾回收期間暫停,這時就會造成應用程序出現短暫的停頓。不過,除非垃圾回收週期很長,否則,這個停頓是不太引人注意的。
爲了儘量避免在不恰當的時間進行垃圾回收,System.GC對象包含一個Collect方法,可在執行關鍵代碼(不希望被GC打斷的代碼)前先調用它,這樣做雖然不能絕對禁止GC執行,但是會顯著減小它運行的可能性---前提是關鍵代碼執行時不會發生內存大量的消耗的情況。

分代:根據對象的生存週期的規律可以發現,相較於長期存在的對象,最近創建的對象更有可能需要被垃圾回收。所以.NET垃圾回收器提供了代(Generation)的概念,它會以更快的頻率回收生存時間較短的對象。而那些已經在前一次垃圾回收中存活下來的對象則會以較低的頻率清除。舉例:如果把對象分爲3代,那麼對象初始時都是0代,當一個對象在垃圾回收週期中存活下來後,都會把它移動到下一代,直至最終移動到第2代(因爲一共有3代)。相比於第二代的對象,垃圾回收器會以更快的頻率對第0代對象進行GC。

三、Mark-Compact 標記壓縮算法

主要處理步驟:將線程掛起→確定roots→創建reachable objects graph→對象回收→heap壓縮→指針修復。

GC搜索roots的地方包括全局對象、靜態變量、局部對象、函數調用參數、當前CPU寄存器中的對象指針(還有finalization queue)等。主要可以歸爲2種類型:已經初始化了的靜態變量、線程仍在使用的對象(stack+CPU register) 。 Reachable objects:指根據對象引用關係,從roots出發可以到達的對象。

  簡單地把.NET的GC算法看作Mark-Compact算法。階段1: Mark-Sweep 標記清除階段,先假設heap中所有對象都可以回收,然後找出不能回收的對象,給這些對象打上標記,最後heap中沒有打標記的對象都是可以被回收的;階段2: Compact 壓縮階段,對象回收之後heap內存空間變得不連續,在heap中移動這些對象,使他們重新從heap基地址開始連續排列,類似於磁盤空間的碎片整理。
Heap內存經過回收、壓縮之後,可以繼續採用前面的heap內存分配方法,即僅用一個指針記錄heap分配的起始地址就可以。
指針修復是因爲compact過程移動了heap對象,對象地址發生變化,需要修復所有引用指針,包括stack、CPU register中的指針以及heap中其他對象的引用指針。

四、Generational 分代算法

程序可能使用幾百M、幾G的內存,對這樣的內存區域進行GC操作成本很高,分代算法具備一定統計學基礎,對GC的性能改善效果比較明顯。將對象按照生命週期分成新的、老的,根據統計分佈規律所反映的結果,可以對新、老區域採用不同的回收策略和算法,加強對新區域的回收處理力度,爭取在較短時間間隔、較小的內存區域內,以較低成本將執行路徑上大量新近拋棄不再使用的局部對象及時回收掉。分代算法的假設前提條件:
A、大量新創建的對象生命週期都比較短,而較老的對象生命週期會更長;
B、對部分內存進行回收比基於全部內存的回收操作要快;


五、GC的兩個主要問題

首先,GC並不是能釋放所有的資源。它不能自動釋放非託管資源。
第二,GC並不是實時性的,這將會造成系統性能上的瓶頸和不確定性。


六、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的對象的內存纔會被回收。

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

  10、GC對不同代的對象執行不同的檢查策略以優化性能。每個GC週期都會檢查第0代對象。大約1/10的GC週期檢查第0代和第1代對象。大約1/100的GC週期檢查所有的對象。重新思考Finalization的代價:需要Finalization的對象可能比不需要Finalization在內存中停留額外9個GC週期。如果此時它還沒有被Finalize,就變成第2代對象,從而在內存中停留更長時間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章