.NET框架垃圾回收機制

(三).NET框架垃圾回收機制
      .NET框架包含一個託管堆,所有的.NET語言在分配引用類型對象時都要使用它。像值類型這樣的輕量級對象始終分配在棧中,但是所有的類實例和數組都被生成在一個內存池中,這個內存池就是託管堆。
     .NET框架中的垃圾回收器被稱爲分代的垃圾回收器(Generational Garbage Collector),也就是說被分配的對象劃分爲3個類別,或稱爲“代”。分別爲0,1,2。0、1、2代對應的託管堆的初始化大小分別是256K,2M和10M。垃圾回收器在發現改變大小能夠提高性能的話,會改變託管堆的大小。例如當應用程序初始化了許多小的對象,並且這些對象會被很快回收的話,垃圾回收器就會將第0代的託管堆變爲128K,並且提高回收的頻率。如果情況相反,垃圾回收器發現在第0代的託管堆中不能回收很多空間時,就會增加託管堆的大小。在應用程序初始化的之前,所有等級的託管堆都是空的。當對象被初始化的時候,他們會按照初始化的先後順序被放入第0代的託管堆中。 
      最近被分配內存空間的對象被放置於第0代,因爲第0代很小,小到足以放進處理器的二級(L2)緩存,所以第0代能夠爲我們提供對其中對象的快速存取。經過一輪垃圾回收後,仍然保留在第0代中的對象被移進第1代中,再經過一輪垃圾內存回收後,仍然保留在第1代中的對象則被移進第2代中。第2代包含了生存期較長的對象,這些對象至少經過了兩輪迴收。
      C#程序爲一個對象分配內存時,託管堆幾乎可以立即返回新對象所需的內存,託管堆之所以能有這樣高效的內存分配性能是由於託管堆較爲簡單的數據結構。託管堆類似於簡單的字節數組,有一個指向第一個可用內存空間的指針。
      在某塊被某對象所請求時,上述指針值就會返回給調用函數,而指針會重新調整至指向下一個可用的內存空間。分配一個託管內存塊只比遞增一個指針的值稍微複雜一點。這也是託管堆所優化的性能之一。在一個不需太多垃圾回收的應用程序中,託管堆的表現會優於傳統的堆。
      由於這個線性的內存分配方法的存在,在C#應用程序中同時分配的對象在託管堆上通常會被分配成彼此相鄰。着安排和傳統的堆內存分配完全不同,傳統的堆內存分配是基於內存塊大小的。例如,兩個同時分配的對象在堆上的位置可能相距很遠,從而降低了緩存的性能。因此雖然內存分配很快,但在一些比較重要的程序中,第0代中的可用內存很有可能會徹底被消耗光。記住,第0代小到可以裝進L2緩衝區,並且沒有被使用的內存不會被自動釋放。當第0代中沒有可以分配的有效內存時,就會在第0代中觸發一輪垃圾回收,在這輪垃圾回收中將刪除所有不再被引用的對象,並將當前正在使用中的對象移至第1代。針對第0代的垃圾回收是最常見的回收類型,而且速度很快。在第0代的垃圾內存回收不能有效的請求到充足的內存時,就啓動第1代的垃圾內存回收。第2代的垃圾內存回收要作爲最後一種手段而使用,當且僅當第1代和第0代的垃圾內存回收不能被提供足夠內存時進行。如果各代都進行了垃圾回收後仍沒有可用的內存,就會引發一個OutOfMemeryException異常 。

 http://www.yesky.com/401/1656401.shtml

[前言:].Net平臺提供了許多新功能,這些功能能夠幫助程序員生產出更高效和穩定的代碼。其中之一就是垃圾回收器(GC)。這篇文章將深入探討這一功能,瞭解它是如何工作的以及如何編寫代碼來更好地使用這一.Net平臺提供的功能。

  .Net中的內存回收機制

  垃圾回收器是用來管理應用程序的內存分配和釋放的。在垃圾回收器出現以前,程序員在使用內存時需要向系統申請內存空間。有些語言,例如Visual Basic,可以自動完成向系統申請內存空間的工作。但是在諸如Visual C++的語言中要求程序員在程序代碼中申請內存空間。如果程序員在使用了內存之後忘了釋放內存,則會引起內存泄漏。但是有了垃圾回收器,程序員就不必關心內存中對象在離開生存期後是否被釋放的問題。當一個應用程序在運行的時候,垃圾回收器設置了一個託管堆。託管堆和C語言中的堆向類似,但是程序員不需要從託管堆中釋放對象,並且在託管堆中對象的存放是連續的。

  每次當開發人員使用 new 運算符創建對象時,運行庫都從託管堆爲該對象分配內存。新創建的對象被放在上次創建的對象之後。垃圾回收器保存了一個指針,該指針總是指向託管堆中最後一個對象之後的內存空間。當新的對象被產生時,運行庫就知道應該將新的對象放在內存的什麼地方。同時開發人員應該將相同類型的對象放在一起。例如當開發人員希望向數據庫寫入數據的時侯,首先需要創建一個連接對象,然後是Command對象,最後是DataSet對象。如果這些對象放在託管堆相鄰的區域內,存取它們就非常快。

  當垃圾回收器的指針指向託管堆以外的內存空間時,就需要回收內存中的垃圾了。在這個過程中,垃圾回收器首先假設在託管堆中所有的對象都需要被回收。然後它在託管堆中尋找被根對象引用的對象(根對象就是全局,靜態或處於活動中的局部變量以及寄存器指向的對象),找到後將它們加入一個有效對象的列表中,並在已經搜索過的對象中尋找是否有對象被新加入的有效對象引用。直到垃圾回收器檢查完所有的對象後,就有一份根對象和根對象直接或間接引用了的對象的列表,而其它沒有在表中的對象就被從內存中回收。

  當對象被加入到託管堆中時,如果它實現了finalize()方法,垃圾回收器會在它的終結列表(Finalization List)中加入一個指向該對象的指針。當該對象被回收時,垃圾回收器會檢查終結列表,看是否需要調用對象的finalize()方法。如果有的話,垃圾回收器將指向該對象的指針加入一個完成器隊列中,該完成器隊列保存了那些準備調用finalize()方法的對象。到了這一步對象還不是真正的垃圾對象。因此垃圾回收器還沒有把他們從託管堆中回收。

  當對象準備被終結時,另一個垃圾回收器線程會調用在完成器隊列中每個對象的finalize()方法。當調用完成後,線程將指針從完成器隊列中移出,這樣垃圾回收器就知道在下一次回收對象時可以清除被終結的對象了。從上面可以看到垃圾回收機制帶來的很大一部分額外工作就是調用finalize()方法,因此在實際編程中開發人員應該避免在類中實現finalize()方法。

  對於finalize()方法的另一個問題是開發人員不知道什麼時候它將被調用。它不像C++中的析構函數在刪除一個對象時被調用。爲了解決這個問題,在.Net中提供了一個接口IDisposable。微軟建議在實現帶有fianlize()方法的類的時侯按照下面的模式定義對象:

public class Class1 : IDisposable 
{
 
public Class1()
 
{
 }


 
~Class1 ()
 
{
  
//垃圾回收器將調用該方法,因此參數需要爲false。
  Dispose (false);
 }


 
//該方法定義在IDisposable接口中。
 public void Dispose ()
 
{
  
//該方法由程序調用,在調用該方法之後對象將被終結。
  
//因爲我們不希望垃圾回收器再次終結對象,因此需要從終結列表中去除該對象。
  GC.SuppressFinalize (this);
  
//因爲是由程序調用該方法的,因此參數爲true。
  Dispose (true);
 }


 
//所有與回收相關的工作都由該方法完成
 private void Dispose(bool disposing)
   
{
  
lock(this//避免產生線程錯誤。
  {
   
if (disposing)
   
{
    
//需要程序員完成釋放對象佔用的資源。
   }


  
//對象將被垃圾回收器終結。在這裏添加其它和清除對象相關的代碼。
 }

}

}

  現在我們瞭解了垃圾回收器工作的基本原理,接下來讓我們看一看垃圾回收器內部是如何工作的。目前有很多種類型的垃圾回收器。微軟實現了一種生存期垃圾回收器(Generational Garbage Collector)。生存期垃圾回收器將內存分爲很多個託管堆,每一個託管堆對應一種生存期等級。生存期垃圾回收器遵循着下面的原則:

  新生成的對象,其生存期越短;而對象生成時間越長的對象,其生存期也就越長。對於垃圾回收器來說,回收一部分對象總是比回收全部對象要快,因此垃圾回收器對於那些生存期短的對象回收的頻率要比生存期長的對象的回收頻率高。

  .Net中的垃圾回收器中目前有三個生存期等級:0,1和2。0、1、2等級對應的託管堆的初始化大小分別是256K,2M和10M。垃圾回收器在發現改變大小能夠提高性能的話,會改變託管堆的大小。例如當應用程序初始化了許多小的對象,並且這些對象會被很快回收的話,垃圾回收器就會將0等級的託管堆變爲128K,並且提高回收的頻率。如果情況相反,垃圾回收器發現在0等級的託管堆中不能回收很多空間時,就會增加託管堆的大小。

  在應用程序初始化的之前,所有等級的託管堆都是空的。當對象被初始化的時候,他們會按照初始化的先後順序被放入等級爲0的託管堆中。在託管堆中對象的存放是連續的,這樣使得託管堆存取對象的速度很快,因爲託管對不必對內存進行搜索。垃圾回收器中保存了一個指針指向託管堆中最後一個對象之後的內存空間。圖一中顯示了一個包含四個對象的0等級的託管堆。


圖一 包含四個對象的託管堆

  當0等級託管堆被對象填滿後,例如候程序初始化了新的對象,使0等級託管堆的大小超過了256K,垃圾回收器會檢查託管堆中的所有對象,看是否有對象可以回收。當開始回收操作時,如前面提到的,垃圾回收器會找出根節點和根節點直接或間接引用了的對象,然後將這些對象轉移到1等級託管堆中,並將0等級託管堆的指針移到最開始的位置以清除所有的對象。同時垃圾回收器會壓縮1等級託管堆以保證所有對象之間沒有內存空隙。當1等級託管堆滿了之後,會將對象轉移到2等級的託管堆。

  例如在圖一之後,垃圾回收器開始回收對象,假定D對象將被回收,同時程序創建了E和F對象。這時候託管堆中的對象如圖二所示。


圖二 回收對象後的0等級和1等級託管堆

  然後程序創建了新的對象G和H,再一次觸發了垃圾回收器。對象E將被回收。這時候託管堆中的對象如圖三所示。



  生存期垃圾回收器的原則也有例外的情況。當對象的大小超過84K時,對象會被放入"大對象區"。大對象區中的對象不會被垃圾回收器回收,也不會被壓縮。這樣做是爲了強制垃圾回收器只能回收小對象以提高程序的性能。

  控制垃圾回收器

  在.Net框架中提供了很多方法使開發人員能夠直接控制垃圾回收器的行爲。通過使用GC.Collect()或GC.Collect(int GenerationNumber)開發人員可以強制垃圾回收器對所有等級的託管堆進行回收操作。在大多數的情況下開發人員不需要干涉垃圾回收器的行爲,但是有些情況下,例如當程序進行了非常複雜的操作後希望確認內存中的垃圾對象已經被回收,就可以使用上面的方法。另一個方法是GC.WaitForPendingFinalizers(),它可以掛起當前線程,直到處理完成器隊列的線程清空該隊列爲止。

  使用垃圾回收器最好的方法就是跟蹤程序中定義的對象,在程序不需要它們的時候手動釋放它們。例如程序中的一個對象中有一個字符串屬性,該屬性會佔用一定的內存空間。當該屬性不再被使用時,開發人員可以在程序中將其設定爲null,這樣垃圾回收器就可以回收該字符串佔用的空間。另外,如果開發人員確定不再使用某個對象時,需要同時確定沒有其它對象引用該對象,否則垃圾回收器不會回收該對象。

  另外值得一提的是finalize()方法應該在較短的時間內完成,這是因爲垃圾回收器給finalize()方法限定了一個時間,如果finalize()方法在規定時間內還沒有完成,垃圾回收器會終止運行finalize()方法的線程。在下面這些情況下程序會調用對象的finalize()方法:

   0等級垃圾回收器已滿

   程序調用了執行垃圾回收的方法

   公共語言運行庫正在卸載一個應用程序域

   公共語言運行庫正在被卸載

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