1. 程式人生 > >C#垃圾回收學習總結

C#垃圾回收學習總結

淺談C#垃圾回收

http://www.cnblogs.com/cuiyiming/archive/2013/03/26/2981931.html


  理解C#垃圾回收機制我們首先說一下CLR(公共語言執行時,Common Language Runtime)它和Java虛擬機器一樣是一個執行時環境,核心功能包括:記憶體管理、程式集載入、安全性、非同步處理和執行緒同步。

CTS(Common Type System)通用型別系統,它把.Net中的型別分為2大類,引用型別與值型別。.Net中所有型別都間接或直接派生至System.Object型別。所有的值型別都是System.ValueType的子類,而System.ValueType本身卻是引用型別。

託管資源:
  由CLR管理的存在於託管堆上的稱為託管資源,注意這裡有2個關鍵點,第一是由CLR管理,第二存在於託管堆上。託管資源的回收工作是不需要人工干預的,CLR會在合適的時候呼叫GC(垃圾回收器)進行回收。

非託管資源:
  非託管資源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 檔案控制代碼, GDI資源, 資料庫連線等等資源(這裡僅僅列舉出幾個常用的)。這些資源GC是不會自動回收的,需要手動釋放。

通過上面的講述總結一下,第一,GC(垃圾回收器)只回收託管資源,不回收非託管資源。第二,GC回收是要在合適的時候(CLR覺得應該進行回收的時候)才進行回收。那麼非託管如何進行回收呢?下面就讓我一一道來。

在.Net中釋放非託管資源主要有2種方式,Dispose,Finalize

Dispose方法,物件要繼承IDisposable介面,也就會自動呼叫Dispose方法。

Class Suifeng:System.IDisposable
{
    #region IDisposable 成員
 
     public void Dispose()
     {
         //
     }
 
     #endregion
}

Suifeng suiFeng= new Suifeng ();
suiFeng.Dispose();


也可以使用Using語句
(using Suifeng suiFeng= new Suifeng())
{
     //
}


Finalize()方法

MSDN上的定義是允許物件在“垃圾回收”回收之前嘗試釋放資源並執行其他清理操作。
它的本質就是解構函式

class Car
{
    ~Car()  // destructor
    {
        // cleanup statements...
    }
}


該解構函式隱式地對物件的基類呼叫 Finalize。 這樣,前面的解構函式程式碼被隱式地轉換為以下程式碼:

protected override void Finalize()
{
    try
    {
        // Cleanup statements...
    }
    finally
    {
        base.Finalize();
    }
}

在.NET中應該儘可能的少用解構函式釋放資源,MSDN2上有這樣一段話:
  實現 Finalize 方法或解構函式對效能可能會有負面影響,因此應避免不必要地使用它們。用 Finalize 方法回收物件使用的記憶體需要至少兩次垃圾回收。當垃圾回收器執行回收時,它只回收沒有終結器的不可訪問物件的記憶體。這時,它不能回收具有終結器的不可


訪問物件。它改為將這些物件的項從終止佇列中移除並將它們放置在標為準備終止的物件列表中。該列表中的項指向託管堆中準備被呼叫其終止程式碼的物件。垃圾回收器為此列表中的物件呼叫 Finalize 方法,然後,將這些項從列表中移除。後來的垃圾回收將確定終止


的物件確實是垃圾,因為標為準備終止物件的列表中的項不再指向它們。在後來的垃圾回收中,實際上回收了物件的記憶體。


  所以有解構函式的物件,需要兩次,第一次呼叫解構函式,第二次刪除物件。而且在解構函式中包含大量的釋放資原始碼,會降低垃圾回收器的工作效率,影響效能。所以對於包含非託管資源的物件,最好及時的呼叫Dispose()方法來回收資源,而不是依賴垃圾回收


器。
   在一個包含非託管資源的類中,關於資源釋放的標準做法是:
   繼承IDisposable介面;
   實現Dispose()方法,在其中釋放託管資源和非託管資源,並將物件本身從垃圾回收器中移除(垃圾回收器不在回收此資源);
   實現類解構函式,在其中釋放非託管資源。
   請看MSDN上的原始碼   

Public class BaseResource:IDisposable
   {
      PrivateIntPtr handle; // 控制代碼,屬於非託管資源
      PrivateComponet comp; // 元件,託管資源
      Privateboo isDisposed = false; // 是否已釋放資源的標誌
       
      PublicBaseResource
      {
      }
        
       //實現介面方法
       //由類的使用者,在外部顯示呼叫,釋放類資源
       Public void Dispose()
       {
           Dispose(true);// 釋放託管和非託管資源
                           
          //將物件從垃圾回收器連結串列中移除,
         // 從而在垃圾回收器工作時,只釋放託管資源,而不執行此物件的解構函式


            GC.SuppressFinalize(this);
         }
        
         //由垃圾回收器呼叫,釋放非託管資源


       ~BaseResource()
        {
           Dispose(false);// 釋放非託管資源
        }
        
     //引數為true表示釋放所有資源,只能由使用者呼叫
    //引數為false表示釋放非託管資源,只能由垃圾回收器自動呼叫
   //如果子類有自己的非託管資源,可以過載這個函式,新增自己的非託管資源的釋放
  //但是要記住,過載此函式必須保證呼叫基類的版本,以保證基類的資源正常釋放
    Protectedvirtual void Dispose(bool disposing)
    {
       If(!this.disposed)// 如果資源未釋放 這個判斷主要用了防止物件被多次釋放
         {
            If(disposing)
            {
               Comp.Dispose();// 釋放託管資源
             }
                           
           closeHandle(handle);// 釋放非託管資源
           handle= IntPtr.Zero;
           }
          this.disposed= true; // 標識此物件已釋放
      }
  }


參考了MSDN和網上的一些資料,第一次寫博文請各位大俠多多指點!


========

c# -- 物件銷燬和垃圾回收

http://www.cnblogs.com/yang_sy/p/3784151.html
有些物件需要顯示地銷燬程式碼來釋放資源,比如開啟的檔案資源,鎖,作業系統控制代碼和非託管物件。在.NET中,這就是所謂的物件銷燬,它通過IDisposal介面來實現。不再使用的物件所佔用的記憶體管理,必須在某個時候回收;這個被稱為無用單元收集的功能由CLR執行



物件銷燬和垃圾回收的區別在於:物件銷燬通常是明確的策動;而垃圾回收完全是自動地。換句話說,程式設計師負責釋放檔案控制代碼,鎖,以及作業系統資源;而CLR負責釋放記憶體。

本章將討論物件銷燬和垃圾回收,還描述了C#處理銷燬的一個備選方案--Finalizer及其模式。最後,我們討論垃圾回收器和其他記憶體管理選項的複雜性。

物件銷燬垃圾回收
1)IDisposal介面
2) Finalizer垃圾回收
物件銷燬用於釋放非託管資源垃圾回收用於自動釋放不再被引用的物件所佔用的記憶體;並且垃圾回收什麼時候執行時不可預計的
為了彌補垃圾回收執行時間的不確定性,可以在物件銷燬時釋放託管物件佔用的記憶體 
 

IDisposal,Dispose和Close

image

.NET Framework定義了一個特定的介面,型別可以使用該介面實現物件的銷燬。該介面的定義如下:

public interface IDisposable
{
void Dispose();
}
C#提供了鴘語法,可以便捷的呼叫實現了IDisposable的物件的Dispose方法。比如:

using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open))
{
// ... Write to the file ...
}
編譯後的程式碼與下面的程式碼是一樣的:


複製程式碼
FileStream fs = new FileStream ("myFile.txt", FileMode.Open);
try
{
// ... Write to the file ...
}
finally
{
if (fs != null) ((IDisposable)fs).Dispose();
}
複製程式碼
finally語句確保了Dispose方法的呼叫,及時發生了異常,或者程式碼在try語句中提前返回。


在簡單的場景中,建立自定義的可銷燬的型別值需要實現IDisposable介面即可


sealed class Demo : IDisposable
{
public void Dispose()
{
// Perform cleanup / tear-down.
...
}
}


請注意,對於sealed類,上述模式非常適合。在本章後面,我們會介紹另外一種銷燬物件的模式。對於非sealed類,我們強烈建議時候後面的那種銷燬物件模式,否則在非sealed類的子類中,也希望實現銷燬時,會發生非常詭異的問題。


物件銷燬的標準語法


Framework在銷燬物件的邏輯方面遵循一套規則,這些規則並不限用於.NET Framework或C#語言;這些規則的目的是定義一套便於使用的協議。這些協議如下:


一旦銷燬,物件不可恢復。物件不能被再次啟用,呼叫物件的方法或者屬性丟擲ObjectDisposedException異常
重複地呼叫物件的Disposal方法會導致錯誤
如果一個可銷燬物件x包含,或包裝,或處理另外一個可銷燬物件y,那麼x的Dispose方法自動呼叫x的Dispose方法,除非另有指令(不銷燬y)
這些規則同樣也適用於我們平常建立自定義型別,儘管它並不是強制性的。沒有誰能阻止你編寫一個不可銷燬的方法;然而,這麼做,你的同事也許會用高射炮攻擊你。


對於第三條規則,一個容器物件自動銷燬其子物件。最好的一個例子就是,windows容器物件比如Form對著Panel。一個容器物件可能包含多個子控制元件,那你也不需要顯示地銷燬每個字物件:關閉或銷燬父容器會自動關閉其子物件。另外一個例子就是如果你在


DeflateStream包裝了FileStream,那麼銷燬DeflateStream時,FileStream也會被銷燬--除非你在構造器中指定了其他的指令。


Close和Stop


有一些型別除了Dispose方法之外,還定義了Close方法。Framework對於Close方法並沒有保持完全一致性,但在幾乎所有情況下,它可以:


要麼在功能上與Dispose一致
或只是Dispose的一部分功能
對於後者一個典型的例子就是IDbConnecton型別,一個Closed的連線可以再次被開啟;而一個Disposed的連線物件則不能。另外一個例子就是Windows程式使用ShowDialog的啟用某個視窗物件:Close方法隱藏該視窗;而Dispose釋放視窗所使用的資源。


有一些類定義Stop方法(比如Timer或HttpListener)。與Dipose方法一樣,Stop方法可能會釋放非託管資源;但是與Dispose方法不同的是,它允許重新啟動。


何時銷燬物件


銷燬物件應該遵循的規則是“如有疑問,就銷燬”。一個可以被銷燬的物件--如果它可以說話--那麼將會說這些內容:


“如果你結束對我的使用,那麼請讓我知道。如果只是簡單地拋棄我,我可能會影響其他例項物件、應用程式域、計算機、網路、或者資料庫”


如果物件包裝了非託管資源控制代碼,那麼經常會要求銷燬,以釋放控制代碼。例子包括Windows Form控制元件、檔案流或網路流、網路sockets,GDI+畫筆、GDI+刷子,和bitmaps。與之相反,如果一個型別是可銷燬的,那麼它會經常(但不總是)直接或間接地引用非託管控制代碼。這


是由於非託管控制代碼對作業系統資源,網路連線,以及資料庫鎖之外的世界提供了一個閘道器(出入口),這就意味著使用這些物件時,如果不正確的銷燬,那麼會對外面的世界程式碼麻煩。


但是,遇到下面三種情形時,不要銷燬物件


通過靜態成員或屬性獲取一個共享的物件
如果一個物件的Dispose方法與你的期望不一樣
從設計的角度看,如果一個物件的Dispose方法不必要,且銷燬物件給程式添加了複雜度
第一種情況很少見。多數情形都可以在System.Drawing名稱空間下找到:通過靜態成員或屬性獲取的GDI+物件(比如Brushed.Blue)就不能銷燬,這是因為該實現在程式的整個生命週期中都會用到。而通過構造器得到的物件例項,比如new SolidBrush,就應該銷燬,這


同樣適用於通過靜態方法獲取的例項物件(比如Font.FromHdc)。


第二種情況就比較常見。下表以System.IO和System.Data名稱空間下型別舉例說明


型別銷燬功能 何時銷燬
MemoryStream防止對I/O繼續操作當你需要再次讀讀或寫流
StreamReader,
StreamWriter清空reader/writer,並關閉底層的流當你希望底層流保持開啟時(一旦完成,你必須改為呼叫StreamWriter的Flush方法)
IDbConnection釋放資料庫連線,並清空連線字串如果你需要重新開啟資料庫連線,你需要呼叫Close方法而不是Dispose方法
DataContext
(LINQ to SQL)防止繼續使用當你需要延遲評估連線到Context的查詢
第三者情況包含了System.ComponentModel名稱空間下的這幾個類:WebClient, StringReader, StringWriter和BackgroundWorker。這些型別有一個共同點,它們之所以是可銷燬的是源於它們的基類,而不是真正的需要進行必要的清理。如果你需要在一個方法中使用這


樣的型別,那麼在using語句中例項化它們就可以了。但是,如果例項物件需要持續一段較長的時間,並記錄何時不再使用它們以銷燬它們,就會給程式帶來不惜要的複雜度。在這樣的情況下,那麼你就應該忽略銷燬物件。


選擇性地銷燬物件


正因為IDisposable實現類可以使用using語句來例項化,因而這可能很容易導致該實現類的Dispose方法延伸至不必要的行為。比如:


public sealed class HouseManager : IDisposable
{
public void Dispose()
{
CheckTheMail();
}
...
}


想法是該類的使用者可以選擇避免不必要的清理--簡單地說就是不呼叫Dispose方法。但是,這就需要呼叫者知道HouseManager類Dispose方法的實現細節。及時是後續添加了必要的清理行為也破壞了規則。


public void Dispose()
{
CheckTheMail(); // Nonessential
LockTheHouse(); // Essential
}
在這種情況下,就應該使用選擇性銷燬模式


public sealed class HouseManager : IDisposable
{
public readonly bool CheckMailOnDispose;
public Demo (bool checkMailOnDispose)
{
CheckMailOnDispose = checkMailOnDispose;
}
public void Dispose()
{
if (CheckMailOnDispose) CheckTheMail();
LockTheHouse();
}
...
}



這樣,任何情況下,呼叫者都可以呼叫Dispose--上述實現不僅簡單,而且避免了特定的文件或通過反射檢視Dispose的細節。這種模式在.net中也有實現。System.IO.Compression空間下的DeflateStream類中,它的構造器如下

public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen)
非必要的行為就是在銷燬物件時關閉內在的流(第一個引數)。有時候,你希望內部流保持開啟的同時並銷燬DeflateStream以執行必要的銷燬行為(清空bufferred資料)


這種模式看起來簡單,然後直到Framework 4.5,它才從StreamReader和StreamWriter中脫離出來。結果卻是醜陋的:StreamWriter必須暴露另外一個方法(Flush)以執行必要的清理,而不是呼叫Dispose方法(Framework 4.5在這兩個類上公開一個構造器,以允許你保持


流處於開啟狀態)。System.Security.Cryptography名稱空間下的CryptoStream類,也遭遇了同樣的問題,當需要保持內部流處於開啟時你要呼叫FlushFinalBlock銷燬物件。


銷燬物件時清除欄位


在一般情況下,你不要在物件的Dispose方法中清除該物件的欄位。然而,銷燬物件時,應該取消該物件在生命週期內所有訂閱的事件。退訂這些事件避免了接收到非期望的通知--同時也避免了垃圾回收器繼續對該物件保持監視。


設定一個欄位用以指明物件是否銷燬,以便在使用者在該物件銷燬後訪問該物件丟擲一個ObjectDisposedException,這是非常值得做的。一個好的模式就是使用一個public的制度的屬性:


public bool IsDisposed { get; private set; }
儘管技術上沒有必要,但是在Dispose方法清除一個物件所擁有的事件控制代碼(把控制代碼設定為null)也是非常好的一種實踐。這消除了在銷燬物件期間這些事件被觸發的可能性。


偶爾,一個物件擁有高度祕密,比如加密金鑰。在這種情況下,那麼在銷燬物件時清除這樣的欄位就非常有意義(避免被非授權元件或惡意軟體發現)。System.Security.Cryptography命令空間下的SymmetricAlgorithm類就屬於這種情況,因此在銷燬該物件時,呼叫


Array.Clear方法以清除加密金鑰。


自動垃圾回收機制


無論一個物件是否需要Dispose方法以實現銷燬物件的邏輯,在某個時刻,該物件在堆上所佔用的記憶體空間必須釋放。這一切都是由CLR通過GC自動處理. 你不需要自己釋放託管記憶體。我們首先來看下面的程式碼


public void Test()
{
byte[] myArray = new byte[1000];
}
當Test方法執行時,在記憶體的堆上分配1000位元組的一個數組;該陣列被變數myArray引用,這個變數儲存在變數棧上。當方法退出後,區域性變數myArray就失去了存在的範疇,這也意味著沒有引用指向記憶體堆上的陣列。那麼該孤立的陣列,就非常適合通過垃圾回收機制進


行回收。


垃圾回收機制並不會在一個物件變成孤立的物件之後就立即執行。與大街上的垃圾收集不一樣,.net垃圾回收是定期執行,盡享不是按照一個估計的計劃。CLR決定何時進行垃圾回收,它取決於許多因素,比如,剩餘記憶體,已經分配的記憶體,上一次垃圾回收的時間。這就


意味著,在一個物件被孤立後到期佔用的記憶體被釋放之間,有一個不確定的時間延遲。該延遲的範圍可以從幾納秒到數天。


垃圾回收和記憶體佔用
垃圾收集試圖在執行垃圾回收的時間與程式的記憶體佔用之間建立一個平衡。因此,程式可以佔用比它們實際需要更多的記憶體,尤其特現在程式建立的大的臨時陣列。
你可以通過Windows工作管理員監視某一個程序記憶體的佔用,或者通過程式設計的方式查詢效能計數器來監視記憶體佔用:
// These types are in System.Diagnostics:
string procName = Process.GetCurrentProcess().ProcessName;
using (PerformanceCounter pc = new PerformanceCounter
("Process", "Private Bytes", procName))
Console.WriteLine (pc.NextValue());
上面的程式碼查詢內部工作組,返回你當前程式的記憶體佔用。尤其是,該結果包含了CLR內部釋放,以及把這些資源讓給作業系統以供其他的程序使用。



根就是指保持物件依然處於活著的事物。如果一個物件不再直接或間接地被一個根引用,那麼該物件就適合於垃圾回收。


一個跟可以是:


一個正在執行的方法的區域性變數或引數(或者呼叫棧中任意方法的區域性變數或引數)
一個靜態變數
存貯在結束佇列中的一個物件
正在執行的程式碼可能涉及到一個已經刪除的物件,因此,如果一個例項方法正在執行,那麼該例項方法的物件必然按照上述方式被引用。


請注意,一組相互引用的物件的迴圈被視作無根的引用。換一種方式,也就是說,物件不能通過下面的箭頭指向(引用)而從根獲取,這也就是引用無效,因此這些物件也將被垃圾回收器處理。


image


Finalizers


在一個物件從記憶體釋放之前,如果物件包含finalizer,那麼finalizer開始執行。一個finalizer的宣告類似構造器函式,但是它使用~字首符號


class Test
{
    ~Test()
    {
        // finalizer logic ...
    }
}


(儘管與構造器的宣告相似,finalizer不能被宣告為public或static,也不能有引數,還不能呼叫其基類)


Finalizer是可能的,因為垃圾收集工作在不同的時間段。首先,垃圾回收識別沒有使用的物件以刪除該物件。這些待刪除的物件如果沒有Finalizer那麼就立即刪除。而那些擁有finalizer的物件會被保持存活並存在放到一個特殊的佇列中。


在這一點上,當你的程式在繼續執行的時候,垃圾收集也是完整的。而Finalizer執行緒卻在你程式執行時,自動啟動並在另外一個執行緒中併發執行,收集擁有Finalizer的物件到特殊佇列,然後執行它們的終止方法。在每個物件的finalizer方法執行之前,它依然非常活躍


--排序行為視作一個跟物件。而一檔這些物件被移除佇列,並且這些物件的fainalizer方法已經執行,那麼這些物件就變成孤立的物件,會在下一階段的垃圾回收過程中被回收。


Finalizer非常有用,但它們也有一些限制:


Finalizer減緩記憶體分配和收集(因為GC需要追蹤那些Finalizer在執行)
Finalizer延長物件及其所引用物件的生命週期(這些物件只有在下一次垃圾回收執行過程中被真正地刪除)
對於一組物件,Finalizer的呼叫順序是不可預測的
你不能控制一個物件的finalizer何時被呼叫
如果一個物件的finalizer被阻塞,那麼其他物件不能處置(Finalized)
如果程式沒有解除安裝(unload)乾淨,那麼finalizer會被忽略
總之,finalizer在一定程度上就好比律師--一旦有訴訟那麼你確實需要他們,一般你不想使用他們,除非萬不得已。如果你使用他們,那麼你需要100%確保你瞭解他們會為你做什麼。


下面是實施finalizer的一些準則:


確保finalizer快速執行
絕對不要在finalier中使用阻塞
不要引用其他可finalizable物件
不要丟擲異常
 
在Finalizer中呼叫Dispose


一個流行的模式是使finalizer呼叫Dispose方法。這麼做是有意義的,尤其是當清理工作不是緊急的,並且通過呼叫Dispose加速清理;那麼這樣的方式更多是一個優化,而不是一個必須。


下面的程式碼展示了該模式是如何實現的


class Test : IDisposable
{
public void Dispose() // NOT virtual
{
Dispose (true);
GC.SuppressFinalize (this); // Prevent finalizer from running.
}
protected virtual void Dispose (bool disposing)
{
if (disposing)
{
// Call Dispose() on other objects owned by this instance.
// You can reference other finalizable objects here.
// ...
}
// Release unmanaged resources owned by (just) this object.
// ...
}
˜Test()
{
Dispose (false);
}
}


Dispose方法被過載,並且接收一個bool型別引數。而沒有引數的Dispose方法並沒有被宣告為virtual,只是在該方法內部呼叫了帶引數的Dispose方法,且傳遞的引數的值為true。


帶引數的Dispose方法包含了真正的處置物件的邏輯,並且它被宣告為protected和virtual。這樣就可以保證其子類可以新增自己的處置邏輯。引數disposing標記意味著它在Dispose方法中被正確的呼叫,而不是從finalizer的最後採取模式所呼叫。這也就表明,如果調


用Dispose時,其引數disposing的值如果為false,那麼該方法,在一般情況下,都會通過finalizer引用其他物件(因為,這樣的物件可能自己已經被finalized,因此處於不可預料的狀態)。這裡面涉及的規則非常多!當disposing引數是false時,在最後採取的模式中


,仍然會執行兩個任務:


釋放對作業系統資源的直接引用(這些引用可能是因為通過P/Invoke呼叫Win32 API而獲取到)


刪除由構造器建立的臨時檔案


為了使這個模式更強大,那麼任何會丟擲異常的程式碼都應包含在一個try/catch程式碼塊中;而且任何異常,在理想狀態下,都應該被記錄。此外,這些記錄應當今可能既簡單又強大。


請注意,在無引數的Dispose方法中,我們呼叫了GC.SuppressFinalize方法,這會使得GC在執行時,阻止finalizer執行。從技術角度講,這沒有必要,因為Dispose方法必然會被重複呼叫。但是,這麼做會改進效能,因為它允許物件(以及它所引用的物件)在單個迴圈


中被垃圾回收器回收。


復活


假設一個finalizer修改了一個活的物件,使其引用了一個“垂死”物件。那麼當下一次垃圾回收發生時,CLR會檢視之前垂死的物件是否確實沒有任何引用指向它--從而確定是否對其執行垃圾回收。這是一個高階的場景,該場景被稱作復活(resurrection)。


為了證實這點,假設我們希望建立一個類管理一個臨時檔案。當類的例項被回收後,我們希望finalizer刪除臨時檔案。這看起來很簡單


複製程式碼
public class TempFileRef
{
public readonly string FilePath;
public TempFileRef (string filePath) { FilePath = filePath; }


~TempFileRef() { File.Delete (FilePath); }
}


實際,上訴程式碼存在bug,File.Delete可能會丟擲一個異常(引用缺少許可權,或者檔案處於使用中) 。這樣的異常會導致拖垮整個程式(還會阻止其他finalizer執行)。我們可以通過一個空的catch程式碼塊來“消化”這個異常,但是這樣我們就不能獲取任何可能發生的錯誤


。 呼叫其他的錯誤報告API也不是我們所期望的,因為這麼做會加重finalizer執行緒的負擔,並且會妨礙對其他物件進行垃圾回收。 我們期望顯示finalization行為簡單、可靠、並快速。


一個好的解決方法是在一個靜態集合中記錄錯誤資訊:


public class TempFileRef
{
static ConcurrentQueue<TempFileRef> _failedDeletions
= new ConcurrentQueue<TempFileRef>();
public readonly string FilePath;
public Exception DeletionError { get; private set; }
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch (Exception ex)
{
DeletionError = ex;
_failedDeletions.Enqueue (this); // Resurrection
}
}
}


把物件插入到靜態佇列_failedDeletions中,使得該物件處於引用狀態,這就確保了它仍然保持活著的狀態,直到該物件最終從佇列中出列。


GC.ReRegisterForFinalize


一個復活物件的finalizer不會再次執行--除非你呼叫GC.ReRegisterForFinalize


在下面的例子中,我們試圖在一個finalizer中刪除一個臨時檔案。但是如果刪除失敗,我們就重新註冊帶物件,以使其在下一次垃圾回收執行過程中被回收。


public class TempFileRef
{
public readonly string FilePath;
int _deleteAttempt;
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch
{
if (_deleteAttempt++ < 3) GC.ReRegisterForFinalize (this);
}
}
}


如果第三次嘗試失敗後,finalizer會靜悄悄地放棄刪除臨時檔案。我們可以結合上一個例子增強該行為--換句話說---那就是在第三次失敗後,把該物件加入到_failedDeletions佇列中。


垃圾回收工作原理


標準的CLR使用標記和緊湊的GC對儲存託管堆上的物件執行自動記憶體管理。GC可被視作一個可被追蹤的垃圾回收器,在這個回收器中,它(GC)不與任何物件接觸;而是被間歇性地被喚醒,然後跟蹤儲存在託管堆物件圖,以確定哪些物件可以被視為垃圾,進而對這些物件


執行垃圾回收。


當(通過new關鍵字)執行記憶體分配是,或當已經分配的記憶體達到了某一閥值,亦或當應用程式佔用的記憶體減少時,GC啟動一個垃圾收集。這個過程也可以通過手動呼叫System.GC.Collect方法啟動。在一個垃圾回收過程中,所有執行緒都可能被凍結。


GC從根物件引用開始,查詢貴根物件對應的整個物件圖,然後把所有的物件標記為可訪問的物件。一旦這個過程完成,所有被標記為不再使用的物件,將被垃圾回收器回收。


沒有finalizer的不再使用的物件立即被處置;而擁有finalizer的不再使用物件將會在GC完成之後,在finalizer執行緒上排隊以等待處理。這些物件(在finalizer執行緒上排隊的物件)會在下一次垃圾回收過程中被回收(除非它們又復活了)。


而那些剩餘的“活”物件(還需要使用的物件),被移動到堆疊開始位置(壓縮),這樣以騰出更多空間容納更多物件。改壓縮過程有兩個目的:其一是避免了記憶體碎片,這樣就使得在為新物件分配空間後,GC只需使用簡單的策略即可,因為新的物件總是分配在堆的尾


部。其二就是避免了維護一個非常耗時的記憶體片段列表任務。


在執行完一次垃圾回收之後,為新物件分配記憶體空間時,如果沒有足夠的空間可以使用,作業系統不能確保更多的記憶體使用時,丟擲OutOfMemoryException。


優化技術


GC引入了各種優化技術來減少垃圾回收的時間。


通用垃圾回收


最重要的優化就是垃圾回收時通用的。其優點是:儘管快速分配和處置大量物件,某些物件是長存記憶體,因此他們不需要被垃圾回收追蹤。


基本上,GC把託管堆分為三類:Gen0是在堆上剛剛分配的物件;Gen1經過一次垃圾回收後仍然存活的物件;剩餘的為Gen2。


CLR限制Gen0的大小(在32位CLR中,最大16MB,一般大小為數百KB到幾MB)。當Gen0空間耗盡,GC便觸發一個Gen0垃圾回收--該垃圾回收發生非常頻繁。對於Gen1,GC也應用了一個相似的大小限制,因為Gen1垃圾回收也是相當頻繁並且快速完成。Gen2包含了所有型別的


垃圾回收,然而,發生在Gen2的垃圾回收執行時間長,並且也不會經常發生。下圖展示了一個完全垃圾回收:


image


如果真要列出一組大概的數字,那麼Gen0垃圾回收執行耗費少於1毫秒,在一個應用程式中一般不會被注意到。而全垃圾回收,如果程式包含大的圖形物件,則可能會耗費100毫秒。執行時間受諸多因素影響二次可能會有不同,尤其是Gen2的垃圾回收,它的尺寸是沒有限


定的。


段時間存活的物件,如果使用GC會非常有效。比如下面示例程式碼中的StringBuilder,就會很快地被髮生在Gen0上的垃圾回收所回收。


string Foo()
{
var sb1 = new StringBuilder ("test");
sb1.Append ("...");
var sb2 = new StringBuilder ("test");
sb2.Append (sb1.ToString());
return sb2.ToString();
}


大物件堆


GC為大物件(大小超過85,000位元組)使用單獨的堆。這就避免了大量消耗Gen0堆。因為在Gen0上沒有大物件,那麼就不會出現分配一組16MB的物件(這些物件由大物件組成)之後,馬上觸發垃圾回收。


大物件堆不適合於壓縮,這是因為發生垃圾回收時,移動記憶體大塊的代價非常高。如果這麼做,會帶來下面兩個後果:


記憶體分配低效,這是因為GC不能總是把物件分配在堆的尾部,它還必須檢視中間的空隙,那麼這就要求維護一個空白記憶體塊連結串列。
大物件堆適合於片段化。這意味著凍結一個物件,會在大物件堆上生成一個空洞,這個空洞很難在再被填充。比如,一個空洞留下了86000位元組的空間,那麼這個空間就只能被一個85000位元組或86000自己的物件填充(除非與另外的一個空洞連線在一起,形成更大的空間)
大物件堆還是非通用的堆,大物件堆上的所有物件被視作Gen2


併發回收和後臺回收


GC在執行垃圾回收時,必須釋放(阻塞)你的程式所使用的執行緒。在這個期間包含了Gen0發生的時間和Gen1發生的時間。


由於執行Gen2回收可能佔用較長的時間,因此GC會在你的程式執行時,堆Gen2回收進行特殊的嘗試。該優化技術僅應用於工作站的CLR平臺,一般應用於windows桌面系統(以及所有執行獨立程式的Windows)。原因是由於阻塞執行緒進行垃圾回收所帶來的延遲對於沒有使用者


介面的伺服器應用程式一般不會帶來問題。


這種對於工作站的優化歷史上稱之為併發回收。從CLR4.0kaishi ,它發生了革新並重命名為後臺回收。後臺回收移除了一個限制,由此,併發回收不再是併發的,如果Gen0部分已經執行完而Gen2回收還正在執行。這就意味著,從CLR4.0開始,持續分配記憶體的應用程式會


更加敏感。


GC通知(適用於服務端CLR)


從Framework 3.5 SP1開始,伺服器版本的CLR在一個全GC將要發生時,向你傳送通知。你可以在伺服器池配置中配置該特性:在一個垃圾回收執行之前,把請求轉向到另外一臺伺服器。然後你立即調查垃圾回收,並等待其完成,在垃圾回收執行完成之後,把請求轉回到


當前伺服器。


通過呼叫GC.RegisterForFullGCNotification,可以啟用GC通知。然後,啟動另外一個執行緒,該執行緒首先呼叫GC.WaitForFullGCApproach,當該方法返回GCNotificationStatus指明垃圾回收已經進入等待執行的佇列,那麼你就可以把請求轉向到其他的伺服器,然後手執


行一次手動垃圾回收(見下節)。然後,你呼叫GC.WaitForFullGCComplete方法,當該方法返回時,GC完成;那麼該伺服器就可以開始再次接收請求。然後在有需要的時候,你可以再次執行上述整個過程。


強制垃圾回收


通過呼叫GC.Collect方法,你可以隨時手動強制執行一次垃圾回收。呼叫GC.Collect沒有提供任何引數會執行一次完全垃圾回收。如果你提供一個整數型別的引數,那麼執行對應的垃圾回收。比如GC.Collect(0)執行Gen0垃圾回收。


// Forces a collection of all generations from 0 through Generation.
//
public static void Collect(int generation) {
    Collect(generation, GCCollectionMode.Default)
}




// Garbage Collect all generations.
//
[System.Security.SecuritySafeCritical]  // auto-generated
public static void Collect() {
    //-1 says to GC all generations.
    _Collect(-1, (int)InternalGCCollectionMode.Blocking);
}


一般地,允許GC去決定何時執行垃圾回收可以得到最好的效能;這是因為強制垃圾回收會把Gen0的物件不必要地推送到Gen1(Gen1不必要地推送到Gen2),從而影響效能。這還會擾亂GC自身的調優能力--在程式執行時,GC動態地調整每種垃圾回收的臨界值以最大限度地


提高效能。


但是,也有另外。最常見的可以執行手動垃圾回收的場景就是當一個應用程式進入休眠狀態,比如執行日常工作的windows服務。這樣的程式可能使用了System.Timters.Timer以每隔24小時觸發一次行為。當該行為完成之後,在接著的24小時之內沒有任何程式碼會執行,那


就意味著,在這段時間內,不會分配任何記憶體,因此GC就沒有機會被啟用。服務在執行時所消耗的任何記憶體,在接著的24小時都會被持續佔用--甚至是空物件圖。那麼解決方法就是在日常的行為完成之後呼叫GC.Collect()方法進行垃圾回收。


為了回收由於finalizer延遲迴收的物件,你可以新增一行額外的程式碼以呼叫WaitForPendingFinalizers,然後再呼叫一次垃圾回收


GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
另外一種呼叫GC.Collect方法的場景是當你測試一個有Finazlier的類時。
 
記憶體壓力


.NET執行時基於一些列因素決定何時啟動垃圾回收,其中一個因素就是機器記憶體的總負載。 如果程式使用了非託管記憶體,那麼執行時會對其記憶體的使用情況持盲目地樂觀的態度,這是因為CLR之關心託管記憶體。通過告訴CLR已經分配了特定量的非託管記憶體記憶體,來減輕


CLR的盲目性;呼叫CG.AddMemoryPresure方法可以完成該目的。如果取消該行為(當所佔用的託管記憶體已經被釋放),那麼可以呼叫GC.RemoveMemoryPressure。


管理記憶體洩漏


在非託管語言中,比如C++,你必須記住當物件不再使用時,應手動地釋放記憶體;否則,將導致記憶體洩漏。在託管世界中,記憶體洩漏這種錯誤時不可能發生的,這歸功於CLR的自動垃圾回收。


儘管如此,大型的和複雜的.NET程式也會出現記憶體洩漏;只不錯記憶體洩漏的方式比較溫和,但具有相同的症狀和結果:在程式的生命週期內,它消耗越來越多的記憶體,到最後導致程式重啟。好訊息是,託管記憶體洩漏通常容易診斷和預防。


託管記憶體洩漏是由不再使用的活物件引起,這些物件之所以存活是憑藉不再使用引用或者被遺忘的引用。一種常見的例子就是事件處理器--它們堆目標物件儲存了一個引用(除非目標是靜態方法)。比如,下面的類:


複製程式碼
class Host
{
public event EventHandler Click;
}
class Client
{
Host _host;
public Client (Host host)
{
_host = host;
_host.Click += HostClicked;
}
void HostClicked (object sender, EventArgs e) { ... }
}
複製程式碼
下面的測試類包含1個方法例項化了1000個Client物件


複製程式碼
class Test
{
static Host _host = new Host();
public static void CreateClients()
{
Client[] clients = Enumerable.Range (0, 1000)
.Select (i => new Client (_host))
.ToArray();
// Do something with clients ...
}
}
複製程式碼
你可能會認為,當CeateClients方法結束後,這個1000個Client物件理解適用於垃圾回收。很不幸,每個Client物件都包含一個引用:_host物件,並且該物件的Click事件引用每個Client例項。 如果Click事件不觸發,那麼就不會引起注意,或者HostClicked方法不做任


何事情也不會引起注意。


解決這個問題的一種方式就是使Client類實現介面IDisposable,並且在dispose方法中,移除時間處理器


public void Dispose() { _host.Click -= HostClicked; }
Client例項的使用者,在使用完例項之後,呼叫Client類的dispose方法處置該例項


Array.ForEach (clients, c => c.Dispose());
下面的對比展示兩種方式的差別


CLR Profiler
Index實現IDisposable未實現IDisposable
Time lineimageimage
Heap statisticsimageimage
GC Generatation Sizesimageimage
 
計時器


不要忘記timmers也會引起記憶體洩漏。根據計時器的種類,會引發兩種不同的記憶體洩漏。首先我們來看System.Timers名稱空間下的計時器。在下面的例子中,Foo類每秒呼叫一次tmr_Elapsed方法


複製程式碼
using System.Timers;
class Foo
{
Timer _timer;
Foo()
{
_timer = new System.Timers.Timer { Interval = 1000 };
_timer.Elapsed += tmr_Elapsed;
_timer.Start();
}
void tmr_Elapsed (object sender, ElapsedEventArgs e) { ... }
}
複製程式碼
很不幸,Foo的例項決定不會被回收。原因在於.NET Framework本身持有對計活動的時器的引用,從而導致.net framework會觸發這些計時器的Elapsed事件。因此


.NET Framework將使_timer處於活動狀態
通過tmr_Elapsed事件處理器,_timer將使Foo實現處於活動狀態
當你意識到Timer實現了IDisposable介面之後,解決的方法就在也明顯不過了。處置Timer例項以停止計時器,並確保.NET Framework不再引用該計時器物件。


class Foo : IDisposable
{
...
public void Dispose() { _timer.Dispose(); }
}
相對於我們上面討論的內容,WPF和Windows窗體的計時器表現出完全相同的方式。


然而,System.Threading名稱空間下的計時器確是一個特例。.NET Framework沒有引用活動執行緒計時器;想法,卻直接引用回撥代理。這就意味著如果你忘記處置執行緒計時器,那麼finalizer會自動觸發並停止計時器然後處置該計時器。比如:


複製程式碼
static void Main()
{
var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000);
GC.Collect();
System.Threading.Thread.Sleep (10000); // Wait 10 seconds
}
static void TimerTick (object notUsed) { Console.WriteLine ("tick"); }
複製程式碼
如果上面的程式碼編譯為釋出模式,那麼計時器會被回收,並且在它再次觸發之前被處置(finalized)。同樣地,我們可以在計時器結束後通過處置該計數器以修復這個問題


using (var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000))
{
GC.Collect();
System.Threading.Thread.Sleep (10000); // Wait 10 seconds
}
using語句會隱式地呼叫tmr.Dispose方法,以確保tmr變數確實處於“使用(活動狀態)”;因此不會在程式碼塊結束之前被當作是死物件。諷刺的是,呼叫Dispose方法實際上使物件存活的時間更長了。


診斷記憶體洩漏


避免託管記憶體洩漏的最簡單方式就是在編寫應用程式時就新增監控記憶體佔用。你可以在程式中通過呼叫下面的程式碼來獲取當前記憶體的使用情況


long memoryUsed = GC.GetTotalMemory (true);
如果你採取測試驅動開發,那麼你可以使用單元測試判斷是否按照期望釋放了記憶體。入股這樣的判斷失敗,那麼接著你就應該檢查你最近對程式所作的修改。


如果你已經有一個大型程式,並且該程式存在託管記憶體洩漏問題,那麼你應該使用windgb.exe工具來幫助你解決問題。當然你還可以使用其他的圖形化工具,比如CLR Profiler, SciTech的Memory Profiler,或者Red Gate的ANTS Memory Profiler。


弱引用


有時候,引用一個對GC而言是“隱形”的物件,並且物件保持活動狀態,這非常有用。這既是弱引用,它由System.WeakReference類實現。使用WeakReference,使用其構造器函式並傳入目標物件。


var sb = new StringBuilder ("this is a test");
var weak = new WeakReference (sb);
Console.WriteLine (weak.Target); // This is a test
如果目標物件僅僅由一個或多個弱引用所引用,那麼GC會把其加入到垃圾回收佇列中。如果目的物件被回收,那麼WeakReference的Target屬相則為NULL。


var weak = new WeakReference(new StringBuilder("weak"))
Console.WriteLine(weak.Target); // weak
GC.Collect();
Console.WriteLine(weak.Target == null); // (true)
為了避免目標物件在測試其為null和使用目標物件之間被回收,把目標物件分配給一個區域性變數


var weak = new WeakReference (new StringBuilder ("weak"));
var sb = (StringBuilder) weak.Target;
if (sb != null) { /* Do something with sb */ }
一旦目標物件分配給一個區域性變數,那麼目的物件就有了一個強型別根物件,從而在區域性變數使用期間不會被回收。


下面例子中的類通過弱引用追蹤所有被例項化的Widget物件,從而使這些例項不會被回收
class Widget
{
static List<WeakReference> _allWidgets = new List<WeakReference>();
public readonly string Name;
public Widget (string name)
{
Name = name;
_allWidgets.Add (new WeakReference (this));
}
public static void ListAllWidgets()
{
foreach (WeakReference weak in _allWidgets)
{
Widget w = (Widget)weak.Target;
if (w != null) Console.WriteLine (w.Name);
}
}
}

這樣一個系統的唯一缺點就是,靜態列表會隨著時間推移而增加,逐漸累積對應null物件的弱引用。因此,你需要自己實現一些清理策略。

弱引用和快取

使用弱引用的目的之一是為了快取大物件圖。通過弱引用,使得耗費記憶體的資料可以進行簡要的快取而不是造成記憶體的大量佔用。

_weakCache = new WeakReference (...); // _weakCache is a field
...
var cache = _weakCache.Target;
if (cache == null) { /* Re-create cache & assign it to _weakCache */ }
在實際上,該策略只會發揮一半的作用,這是因為你不能控制GC何時執行,並且也不能控制GC會會執行哪一類回收。尤其是,當你的快取是在Gen0中,那麼這類記憶體會在微妙級別類被回收。因此,至少,你需要使用兩類快取,通過它們,首先你擁有一個強型別,然後不

時地把該強型別轉換成弱型別。


弱引用和事件


在前面的章節中,我們看到事件是如何引起記憶體洩漏。而且解決這種記憶體洩漏的最簡單方法是避免時間訂閱,或者對為訂閱事件的物件實現Dispose方法。此外,弱引用也提供了另外一種解決方案。


假設一個帶來對其目標持有一個弱引用。那麼這樣的一個代理並不會使其目標為活動狀態,除非這些目標物件有獨立的引用。當然,這並不會阻止一個被觸發的代理,在目標物件進入回收佇列之後但在GC開始對該目標物件執行回收前的時間段中,擊中一個未被引用的目


標。為了該方法高效,你的程式碼必須非常穩定。下面的程式碼就是就是採用這種方式的具體實現:


複製程式碼
public class WeakDelegate<TDelegate> where TDelegate : class
{
class MethodTarget
{
public readonly WeakReference Reference;
public readonly MethodInfo Method;
public MethodTarget (Delegate d)
{
Reference = new WeakReference (d.Target);
Method = d.Method;
}
}
List<MethodTarget> _targets = new List<MethodTarget>();
public WeakDelegate()
{
if (!typeof (TDelegate).IsSubclassOf (typeof (Delegate)))
throw new InvalidOperationException
("TDelegate must be a delegate type");
}
public void Combine (TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
_targets.Add (new MethodTarget (d));
}
public void Remove (TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
{
MethodTarget mt = _targets.Find (w =>
d.Target.Equals (w.Reference.Target) &&
d.Method.MethodHandle.Equals (w.Method.MethodHandle));
if (mt != null) _targets.Remove (mt);
}
}
public TDelegate Target
{
get
{
var deadRefs = new List<MethodTarget>();
Delegate combinedTarget = null;
foreach (MethodTarget mt in _targets.ToArray())
{
WeakReference target = mt.Reference;
if (target != null && target.IsAlive)
{
var newDelegate = Delegate.CreateDelegate (
typeof (TDelegate), mt.Reference.Target, mt.Method);
combinedTarget = Delegate.Combine (combinedTarget, newDelegate);
}
else
deadRefs.Add (mt);
}
foreach (MethodTarget mt in deadRefs) // Remove dead references
_targets.Remove (mt); // from _targets.
return combinedTarget as TDelegate;
}
set
{
_targets.Clear();
Combine (value);
}
}
}
複製程式碼
上述程式碼演示了許多C#和CLR的有趣的地方。首先,我們在構造器中檢查了TDelegate是一個代理型別。這是因為C#本身的限制--因為下面的語句不符合C#的語法


... where TDelegate : Delegate // Compiler doesn't allow this
由於必須要進行型別限制,所以我們在構造器中執行執行時檢查。


在Combine方法和Remove方法中,我們執行了引用轉換,通過as運算子(而沒有使用更常見的轉換符)把target物件轉換成Delegate型別。這是由於C#不允許轉換符使用型別引數--因為它不能分清這是一個自定義的轉換還是一個引用抓換(下面的程式碼不能拖過編譯)。


foreach(Delegate d in ((Delegate)target).GetInvocationList())
                _targets.Add(new MethodTarget(d));
當呼叫GetInvocationList,由於這些方法可能被一個多播代理呼叫,多播代理就是一個代理有多餘一個的方法接收。


對於Target屬性,我們使其為一個多播代理--通過一個弱引用包含所有的代理引用,從而使其目標物件保持活動。然後我們清楚剩餘的死引用,這樣可以避免_targets列表無限制的增長。下面的程式碼演示瞭如何使用我們上面建立的實現了事件的代理類:


複製程式碼
public class Foo
{
WeakDelegate<EventHandler> _click = new WeakDelegate<EventHandler>();
public event EventHandler Click
{
add { _click.Combine (value); } remove { _click.Remove (value); }
}
protected virtual void OnClick (EventArgs e)
{
EventHandler target = _click.Target;
if (target != null) target (this, e);
}
}
複製程式碼
請注意,在觸發事件時,在檢查和呼叫之前,我們把_click.Target物件賦值給一個臨時變數。這就避免了目標物件被GC回收的可能性。


參考


http://msdn.microsoft.com/en-US/library/system.idisposable.aspx


========

關於C#中垃圾回收GC雜談

http://blog.csdn.net/pan869823184/article/details/19299581


在初學階段用.Net編寫程式時,一直都未曾考慮過程式垃圾資源回收率的問題,那是因為老師老在課堂講什麼不用管,不用理會,一聽到不用理會,好吧,從此寫程式就肆無忌憚的了!程式卡死、記憶體暴漲、順便偶爾來幾個記憶體錯誤,一看到這個就頭大了。現在想想,


課堂老師講的那句話,卻只聽進了前半句。。。
閒聊無事,也不用再怕什麼在職防止洩露啥啥機密、啥啥技術的、、、嘎嘎、、、、(下面的純屬個人觀點,如有雷同、敬請繞道、、、)
在.Net裡面垃圾收集的工作方式:


執行.NET應用程式時,程式創建出來的物件例項都會被CLR跟蹤,CLR都是有記錄哪些物件還會被用到(存在引用關係);哪些物件不會再被用到(不存在引用關係)。CLR會整理不會再被用到的物件,在恰當的時機,按一定的規則銷燬部分物件,釋放出這些物件所佔用的


記憶體。


說到這裡,那就引出了新的技術點:


CLR是怎麼記錄物件引用關係的?


CLR會把物件關係做成一個“樹圖”,這樣標記他們的引用關係


CLR是怎麼釋放物件的記憶體的?


關鍵的技術是:CLR把沒用的物件轉移到一起去,使記憶體連續,新分配的物件就在這塊連續的記憶體上建立,這樣做是為了減少記憶體碎片。注意!CLR不會移動大物件


垃圾收集器按什麼規則收集垃圾物件?


CLR按物件在記憶體中的存活的時間長短,來收集物件。時間最短的被分配到第0代,最長的被分配到第2代,一共就3代。


一般第0貸的物件都是較小的物件,第2代的物件都是較大的物件,第0代物件GC收集時間最短(毫秒級別),第2代的物件GC收集時間最長。當程式需要記憶體時(或者程式空閒的時),GC會先收集第0代的物件,


收集完之後發現釋放的記憶體仍然不夠用,GC就會去收集第1代,第2代物件。(一般情況是按這個順序收集的)


如果GC跑過了,記憶體空間依然不夠用,那麼就丟擲了OutOfMemoryException異常。


GC跑過幾次之後,第0代的物件仍然存在,那麼CLR會把這些物件移動到第1代,第1代的物件也是這樣。


既然有了垃圾收集器,為什麼還要Dispose方法和解構函式?


因為CLR的緣故,GC只能釋放託管資源,不能釋放非託管資源(資料庫連結、檔案流等)。


那麼該如何釋放非託管資源呢?


一般我們會選擇為類實現IDispose介面,寫一個Dispose方法。


讓呼叫者手動呼叫這個類的Dispose方法(或者用using語句塊來自動呼叫Dispose方法)


Dispose執行時,解構函式和垃圾收集器都還沒有開始處理這個物件的釋放工作


有時候,我們不想為一個型別實現Dispose方法,我們想讓他自動的釋放非託管資源。那麼就要用到析構函數了。


解構函式是個很奇怪的函式,呼叫者無法呼叫物件的解構函式,解構函式是由GC呼叫的。


你無法預測解構函式何時會被呼叫,所以儘量不要在這裡操作可能被回收的託管資源,解構函式只用來釋放非託管資源


GC釋放包含解構函式的物件,比較麻煩(需要幹兩次才能幹掉她),


CLR會先讓解構函式執行,再收集它佔用的記憶體。


我們需要手動執行垃圾收集嗎?什麼場景下這麼做?


GC什麼時候執行垃圾收集是一個非常複雜的演算法(策略)


大概可以描述成這樣:


如果GC發現上一次收集了很多物件,釋放了很大的記憶體,


那麼它就會盡快執行第二次回收,


如果它頻繁的回收,但釋放的記憶體不多,


那麼它就會減慢回收的頻率。


所以,儘量不要呼叫GC.Collect()這樣會破壞GC現有的執行策略。


除非你對你的應用程式記憶體使用情況非常瞭解,你知道何時會產生大量的垃圾,那麼你可以手動干預垃圾收集器的工作 


我有一個大物件,我擔心GC要過很久才會收集他,


[csharp] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  
關於弱引用和垃圾收集之間的關係?


當一個大物件被使用後不存在引用關係時,GC就會自動回收它佔用的記憶體。


當這個物件足夠大的情況下,GC在回收它時,可能時間稍微會長點,當用戶需要再次使用該物件時,我們可以從回收池中再次提取該物件,這裡就涉及到弱引用,程式碼如下:




[csharp] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
var bss = new BsCtl(BrowserContainer);  
            var vbss = new WeakReference<BsCtl>(bss);  
            bss = null;  
            BsCtl ok;              
            vbss.TryGetTarget(out ok);  
            //如果沒有進行垃圾收集OK不會為NULL  
            if (ok == null)  
            {  
                //如果已經進行了垃圾收集,就會執行這段程式碼  
                ok = new BsCtl(BrowserContainer);  
            }  




垃圾收集隨時可以收集bss物件,


如果收集了,就會進入if語句塊,如果沒有收集,就不會進入if語句塊,TryGetTarget(out ok)就成功把bss從垃圾堆裡撈回來了。


注意:這裡只說了短弱引用,沒有提及長弱引用,我覺得長弱引用使用的場景較少。


垃圾收集器優點:


因為我沒有很豐富的C/C++程式設計經驗,如果想談垃圾收集器的好處,那麼勢必要和C/C++這樣的較低階的語言對比。所以一般性的回答都是減少記憶體使用不當的BUG,提升程式設計效率之類的問題。
========

垃圾收集器原理

http://www.tuicool.com/articles/Nbyqi2
原文  http://www.cnblogs.com/izhaogang/p/collector.html


在程式設計的過程中,你是否遇到過OutOfMemeryException的異常?程式在做效能測試時,應用伺服器程式消耗的記憶體不斷上升?在使用開源框架時,由於沒有及時的Dispose而導致程式的異常發生?而造成這些異常的原因都是沒有合理的釋放記憶體導致的。我們不經要問,C#


框架下不是有GC(自動垃圾收集器)嗎?那麼為什麼還會出現如此異常錯誤呢?GC到底何時執行,執行時又做了什麼?GC對效能的影響?怎樣合理的釋放資源呢?下面我們來揭開垃圾收集器的神祕面紗。


1、垃圾收集平臺的基本工作原理


1.1 基本原理分析


我們知道,C#是CLR(Common Language Runtime公共語言執行庫)下的一種託管程式碼語言,它的型別和物件在應用計算機記憶體時,大體用到兩種記憶體,一種叫堆疊,另一種叫託管堆。C#中主要分為值型別和引用型別,當宣告一個值型別物件時,會在棧中分配適當大小的


記憶體,記憶體空間儲存物件的值。其中維護一個棧指標,它包含棧中下一個可用記憶體空間的地址。當一個變數離開作用域時,棧指標向下移動並釋放變數所佔用的記憶體,所以它任然指向下一個可用地址;當宣告一個引用型別物件時,引用變數也利用棧,但這時棧包含的只是


對另一個記憶體位置的引用,而不是實際的值。這個位置是託管堆中的一個地址,和棧一樣,它也維護一個指標,包含堆中下一個可用記憶體空間的地址。我們來寫一個簡單的事例程式碼,看看它內部到底發生了什麼?

namespace SourceDemo
{
	class Program
	{ 
		static void Main(string[] args)
		{
			int iTotal = 1;
			Order order = new Order();
		}
	}
	class Order
	{
	}
}


通過ILDASM.EXE工具檢視對應的IL程式碼如下:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint					
  // Code size 10 (0xa)
  .maxstack  1  
  .locals init ([0] int32 iTotal,
	  [1] class SourceDemo.Order order)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  newobj     instance void SourceDemo.Order::.ctor()
  IL_0008:  stloc.1
  IL_0009:  ret
} // end of method Program::Main


可以看到,宣告引用型別和值型別的區別在於引用型別有一個newObj建立物件的操作。那麼newObj到底做了哪些操作呢?主要操作如下:


計算新建物件所需要的記憶體總數(包括基類的所有欄位位元組總數)。
在前面所得位元組總數的基礎上再加上物件開銷所需的位元組數。開銷包括:型別物件指標和同步塊的索引。
CLR檢查保留區域是否有足夠的空間來存放新建物件。


如果空間足夠,呼叫型別的建構函式,將物件存放在NextObjPtr指向的記憶體地址中。
如果空間不夠,就執行一次垃圾回收來清理託管堆,如果依然不夠,則丟擲OutOfMemeryException異常
最後,移動NextObjPtr指向託管堆下一個可用地址。可以看到,垃圾收集器通過檢查託管堆上不再使用的物件來回收記憶體,那麼垃圾收集器怎麼確定物件是不再使用的物件呢?請接著往下看。


1.2 應用程式的根


每個應用程式都有一組根,一個根就是一個儲存物件,其中包含一個指向引用型別的記憶體指標,它或者指向託管堆的物件,或者被設為null。如類欄位、方法引數或者是區域性變數都是根,注意只有引用型別才被認為是根,而值型別只是佔用記憶體永遠不會被認為為根。垃


圾收集器是怎麼工作的呢?工作主要分為以下兩階段:


第一階段,標記物件階段。 
垃圾收集器開始執行的時候,首先假設託管堆中的物件都是可以收集的垃圾。它開始遍歷執行緒的堆疊檢查所有的根,如果發現根引用了一個物件那麼就在該物件的同步塊的索引欄位上設定一位來標記它。同時檢查該物件是否引用其他物件,如果引用則進行標記,通過遞


歸的方式進行標記,直到發現根及根引用的物件已經標記,垃圾收集器將繼續收集下一個根。
第二階段,壓縮階段。該階段垃圾收集器線性的遍歷堆以尋找包含未標記物件的連續區塊。如果垃圾收集器找到了較小記憶體塊,那麼它忽略記憶體不計;如果找到了較大的連續記憶體塊,那麼垃圾收集器將把記憶體中非垃圾物件搬移到這些連續記憶體塊中以壓縮託管堆。


圖:垃圾收集器執行前的託管堆。


對於以上描述,專業詞彙較多不是蠻好理解。我們來舉一個容易理解的例子:有一個執行清理房間的任務(任務方法),房間中有很多物品、櫃子盒子及其裡面的物品等都需要清理(物件清理),當我們執行這個任務時(呼叫方法),清理過程中我們可以標記物品,同


時可能存在這樣的情況,我們在清理其中一個盒子的時候,發現盒子裡面還有其他的盒子,如手機盒子裡面還有個裝充電器的盒子(手機裡面又引用了