.NET中的記憶體回收
cloud(翻譯) 來源:CSDN
目錄
l 導言
l 關於記憶體回收
l 記憶體回收演算法
m 應用程式根(Application Roots)
l 實現
m 階段I: 標記(Mark)
m 階段II: 整理(Compact)
l 終結(Finalization)
l 記憶體回收效能的最佳化
m 弱引用(WeakReference)
m 代(Generations)
l 記憶體回收相關的神話
導言
微軟聲稱.NET是一種革命性的編程技術。許多要素使它成為大多數開發人員的首選。本文我們將要討論一下.NET Framework中一個很主
要的優勢——記憶體和資源管理的便捷性。
關於記憶體回收行程
每個程式都會使用一定次序的資源,或記憶體緩衝區,或網路連接,或資料庫資源等等。實際上,在物件導向環境中,每種類型都被看作
是程式的某些有效資源。為了使用這些資源,必須分配部分記憶體來描述這種類型。
資源訪問按一下步驟進行:
1. 為類型分配記憶體來描述資源。
2. 初始化資源,把資源設定成初始化狀態,使資源可用。
3. 通過訪問類型執行個體的成員來使用資源(按需求重複)。
4. 銷毀資源狀態以清除資源。
5. 釋放記憶體。
.NET中的記憶體回收行程(GC)完全徹底協助開發人員從追蹤記憶體的使用和確定什麼時候釋放內中解脫出來。
Microsoft .NET CLR (公用語言運行庫)要求所有資源都從託管堆中進行分配。你無需釋放託管堆中的對象——當應用程式不再需要這些對象
時,對象將被自動釋放。
記憶體不是無限的。記憶體回收行程需要執行回收以便釋放記憶體。記憶體回收行程的最佳化引擎會對已做的分配選擇最好的回收時間(準確標準由微軟提供
)。當記憶體回收行程執行回收時,它先找出託管堆中不再被應用程式使用的對象然後執行相應操作收回這些對象的記憶體空間。
然而,為了進行自動記憶體管理,GC必須知道根的位子。也就是說,它應該知道一個對象什麼時候不再被應用程式使用了。在.NET中,GC
是通過一個稱之為中繼資料的東西瞭解到這些的。.NET中使用的每種資料類型都通過中繼資料來描述它。在中繼資料的協助下,CLR知道記憶體中每個對
象的布局,在記憶體回收的整理階段給GC提供協助。沒有這些資訊,GC將不會知道一個對象在哪兒結束和下一個從哪兒開始。
記憶體回收演算法
應用程式根(Application Roots)
每個應用程式都有一套根(Roots)。根標識儲存位置,這個位置或者指向一個託管堆的對象,或者指向一個Null 物件(null)。
比如:
l 一個應用程式中所有全域和靜態對象指標。
l 一個線程堆棧中所有局部變數/參數對象指標。
l 託管堆中所有CPU登記的對象指標。
l FReachable隊列中的對象指標。
活動根的表由JIT編譯器和CLR維護,並且對於記憶體回收行程的演算法是訪問。
實現
.NET中的記憶體回收是用跟蹤回收實現的,確切的說CLR實現了標記(Mark)/整理(Copact)回收器。
這個方法有以下兩個階段組成:
階段I:標記(Mark)
找到可以被收回的記憶體。
當GC開始運行時,它假設堆中的所有對象都是垃圾。換句話說,它假設應用程式的根沒有指向堆中的的任何對象。
階段I中包含下列步驟:
1. GC識別存活對象的引用或應用程式根。
2. 從根開始遍曆,建一張可以從根遍曆的所有對象的圖。
3. 如果GC準備嘗試添加一個已經在圖中的對象,它就停止這條路徑的遍曆。這樣做有兩個目的,第一個是極大的最佳化效能,因為它不會遍曆
一套對象一次以上。第二是防止當有對象的迴圈串連列表時而發生死迴圈,因此迴圈被有效控制了。
一旦所有的根都被檢查完後,記憶體回收行程的圖中包含了所有可以從應用程式根遍曆到的對象。任何不再圖中的對象都不能被應用程式訪問到,
也就是所謂的垃圾。
階段II:整理(Compact)
把所有存活的對象移到堆的末端,空出堆頂端的空間。
階段II包含下列步驟:
1. 現在GC線性地遍曆堆,尋找鄰近的垃圾對象塊(現在被認為是空閑空間)。
2. 然後GC往下移動記憶體中的非垃圾對象,去掉堆中的所有空隙。
3. 移動記憶體中的對象導致對象指標失效。因此GC需要修改應用程式的根使對象的指標指向新的位置。
4. 另外,如果對象包含一個指向其它對象的指標,GC也會負責糾正這些指標。
在所有垃圾被標識完以後,所有的非垃圾對象也被整理,並且所有非垃圾對象的指標也被修正,最後一個非垃圾對象後的指標指向下一個被添
加對象的位置。
終結(Finalization)
.NET Framework的記憶體回收行程能暗中追蹤由應用程式建立的對象的生命週期,但是當它遇到對象封裝了非託管資源(比如檔案、視窗或網路連
接等)時卻無能為力。
一旦應用程式不再使用那些非託管資源時需要顯示地釋放它們。.NET Framework為對象提供了終結(Finalize)方法:在記憶體回收行程收回這個對
象的記憶體時,必須執行對象的這個方法來清除它的非託管資源。由於預設的Finalize方法什麼都沒做,如果需要顯示清除資源必須覆蓋這個方
法。
如果一把Finalize方法當作只是C++中解構函式另外一個名字那也不足為怪。雖然它們都被賦予了釋放對象佔有的資源的任務,但是它們還是有
很不相同的語義。在C++中,當對象推出範圍時解構函式會立刻被調用,而Finalize方法是在起動記憶體回收清除對象時才被調用的。
.NET中,由於終結器(Finalizer)的存在使得記憶體回收的工作變得更加複雜了,因為它在釋放對象前增加了許多額外的操作。
無論什麼時候,在堆上分配一個含有Finalize方法的新對象時,都會有一個指向這個對象的指標被添加到一個稱為Finalization隊列的內部數
據結構當中。當對象不能再次被遍曆到時,GC就認為這個對象是垃圾。GC首先掃描Finalization隊列尋找這些對象的指標,當指標被找到時,
把它從Finalization隊列中去掉並添加到另外一個名為FReachable隊列的內部資料結構中,使這個對象不再是垃圾的一部分。這時,GC完成了
確定垃圾。然後整理(Compact)可收回的記憶體,由專門的線程負責清空FReachable隊列並執行對象的Finalize方法。
第二次記憶體回收行程被觸發的時候,它把被終結(Finalize)的對象看作真正的垃圾,然後簡單的釋放它們的記憶體。
由此可知當一個對象需要終結時,它先死,然後存活(複活),然後再次並且最終地死去。推薦避免使用Finalize方法,除非有需要。
Finalize方法會增加記憶體壓力,因為直到兩次記憶體回收被啟動時,對象佔用的記憶體和資源才會得到釋放。因此你無法控制兩次Finalize方法執
行的順序,它可能會導致無法預料的後果。
記憶體回收效能的最佳化
l 弱引用(WeakReference)
l 代(Generations)
弱引用(WeakReference)
弱引用(WeakReference)是提高效能的一種方式,用於減少託管堆中大對象的壓力。
當一個根指向一個對象時,它被稱為這個對象的一個強引用並且這個對象不能被回收,因為應用程式能遍曆到這個對象。
當一個對象有一個指向它的弱引用(WeakReference)時,基本上是指如果有記憶體請求並且GC啟動時,這個對象可以被回收,當應用程
序再次嘗試去訪問這個對象時,訪問將會失敗。另一方面,為了能訪問一個被弱引用(WeakReference)的對象,應用程式必須獲得一個對這個
對象的強引用。如果應用程式在記憶體回收行程回收這個對象之前獲得了它的強引用,GC將不能回收這個對象,因為有這個對象的強引用存在。
託管堆包含兩個管理弱引用(WeakReference)的內部資料結構:短弱參考資料表和長弱參考資料表。
兩種類型的弱引用:
l 短弱引用不追蹤複蘇。
也就是說,一個有短弱引用的對象會被立即收回,而不用等到運行Finalize方法。
l 長弱引用追蹤複蘇。
也就是說,只有當長弱參考資料表中的對象的儲存空間可收回的時候GC才回收這個對象。如果對象有Finalize方法,是在Finalize方法被調用了之
後並且對象不能複活了。
這兩個表簡單的存放著分配在託管堆中對象的指標。最初,兩個表均為空白。當你建立一個弱引用(WeakReference)對象時,對象不從托
管堆中分配。而是在一個弱參考資料表中分配一個空的儲存位置;短弱引用使用短弱參考資料表,長弱引用使用長弱參考資料表。
讓我們看一個例子,看看GC運行時會發生些什麼。下面的圖(圖1和圖2)顯示了GC運行前和運行後所有內部資料結構的狀態。
圖1:GC運行前
圖2:GC運行後
以下是GC運行時進行的操作:
1. GC為所有可遍曆的對象建一張圖。在上面的例子中,圖包含對象B,C,E,G。
2. GC掃描短弱參考資料表。如果表中指標指向的對象不在圖中,那麼這個指標標識的是一個不可遍曆的對象,短弱參考資料表中的這個位置置為null
。在上面的例子中,對象D的位置置為null,因為它不是圖的一部分。
3. GC掃描Finalization隊列。如果隊列中的指標所指的對象不在圖中,那麼這個指標標識一個不可遍曆的對象,指標從Finalization隊列中
移到FReachable隊列中。這時,對象被認為是可遍曆的,所以加到圖中。在上面的例子中,對象A,D,F是不包含在圖中但看作是可遍曆的對象,
因為它們屬於Finalization隊列。進而Finalization隊列被清空。
4. GC掃描長弱參考資料表。如果表中的指標指的對象不在圖中(現在圖包括FReachable隊列中指標所指的對象),那麼指標標識一個不可遍曆的
對象,所在位置置為null。由於對象C和F都包含在圖中,都不置null。
5. GC整理(Compact)記憶體,擠出不可遍曆對象留下的空隙。在上面的例子中,對象H是唯一從堆中刪除的對象,它所分配的記憶體被收回。
代(Generations)
由於記憶體回收要在停止整個程式的情況下才能完成,它們可能會在程式執行期間進行任意長時間的中斷。GC也有可能中斷為滿足即時系
統的需求而要求及時響應的事件。
GC中有一個特徵叫代(Generations),就是專門為提高效能而設計的。一個多代的GC是通過對觀察用各種語言編寫的大部分程式而得到
兩個事實進行仔細分析而得到的:
1. 新建立的對象擁有更短的生命週期。
2. 越老的對象,存活的越久。
多代的回收器通過對象的年齡把它分成若干組,並且年輕的對象比年老的對象回收的更頻繁。初始化時,託管堆不含任何對象。所有新的對象
都被添加到第0代堆中,直到堆裝滿了並觸發記憶體回收。由於大部分對象存活的時間很短,只有一小部分年輕的對象在第一次回收時存活下來。
一旦一個對象在第一次回收後存活下來後,它就被提升到第1代。在記憶體回收後可以說新的對象都在第0代堆中。只有當第0代的堆裝滿時垃圾回
收才會再次被觸發。所有第0代存活下來的對象被整理提升到第1代中。然後第0代不含任何對象了,但是所有新的對象都進入了第0代。
因此,作為當前代中“成熟”(存活於多代回收器)的對象,它們都會被移到下一級更老的代中。第2代是CLR的GC所支援的最大的代。以後回
收時,第2代存活的對象將只是簡單的停留在第2代。
因此,把堆劃分成對象的代並且回收和整理更年輕的代中的對象提高了記憶體回收演算法的效率,因為從堆中收回了大量的有意義的空間,同時比
起回收器檢查所有代中的所有對象要快得多。
一個能執行多代回收的GC,每次回收都要確保(至少要儘可能)所需時間小於某個最大時間,以協助為即時環境能做一些配套的即時操作,同
時也防止出現讓使用者明顯感覺到的中斷現象。
記憶體回收相關神話
GC顯然比手工記憶體管理要慢
對應解釋:不是一定的。現代記憶體回收行程看起來運行時和手工儲存分配(malloc/free或new/delete)一樣快。在一些特殊的程式中,記憶體回收
啟動並執行可能不如為使用者專門設計的自訂記憶體配置那麼快。但從另一方面說,為了使手工記憶體管理正確的工作而添加的額外代嗎(比如說,顯
示的引用計數)常常比GC所做的要昂貴的多。
GC會使程式中斷
對應解釋:由於記憶體回收行程在尋找和回收垃圾對象時通常停止整個應用程式,他們可能會導致停機時間過長而讓使用者覺察到。但是通過現在優
化計數,這些可以感覺到的中斷完全可以避免。
手工記憶體管理不會導致中斷
對應解釋:手工記憶體管理並不能確保效能。它可能由於大量的分配或釋放記憶體工作而導致中斷。
使用GC的程式很大並且臃腫;GC不適合小的程式或系統
對應解釋:儘管在複雜的系統中使用GC很有優勢,也沒有理由認為GC在其它尺寸的程式中會引入什麼大的開銷。
我已經聽說了GC會使用兩次大量的記憶體
對應解釋:對於原來的GC這可能是個事實,但並不是記憶體回收行程都是這樣的。用於GC的資料結構比那些手工記憶體管理的要大的多。