淺談C#記憶體回收與Dispose﹐Close﹐Finalize方法[轉]

iDotNetSpace發表於2009-08-07

.net記憶體回收與Dispose﹐Close﹐Finalize方法

一. net的物件使用一般分為三種情況﹕

1.建立物件
2.使用物件
3.釋放物件

二.建立物件
1.建立物件實際分為兩個步驟﹕變數型別宣告和初始化物件

2.變數型別宣告(declare),如﹕

FileStream fs

這行程式碼會在當前的變數作用域空間(棧或堆)裡建立一個叫做fs的變數﹐至少四個位元組吧(因為要存一個物件的地址)

3.初始化物件
物件在使用(呼叫其方法或屬性)前﹐必須進行初始化。
如﹕

fs = new FileStream(@"C: est.txt",FileMode.OpenOrCreate);

這行程式碼會分成3個步驟﹕
a.在託管堆中分配一塊記憶體﹐其大小等於FileStream中所有欄位(當然不包括靜態的)的記憶體總和加上MS認為需要的其它東東。
b.初始化物件的欄位(值型別的把其位全部初始化成0,物件初始化為null﹐當然string是一個例外﹐它被初始化成空字串)
c.呼叫FileStream相應的構造器﹐這裡會初始化一個非託管資源(檔案)的私有欄位。

三.使用物件
使用物件就沒什麼講的﹐就是呼叫物件的方法(或屬性等)來完成某個功能當然為了釋放物件而呼叫的方法其範疇應不屬於此類中(現在提到的Finalize等)

四.釋放物件
1.釋放物件也就是說這個物件我已經不需要了﹐現在我要把其釋放﹐以便把其在堆上所佔用的記憶體空間給收回來(當然變數名的記憶體空間就不需要管了﹐因為它會隨其作用域自動消失)

2. .net自動進行記憶體管理﹐也就是說當它判斷一個物件沒有用了(當然有自己的演算法)﹐它就會將其記憶體給自動收回來﹐但是其收回的時間一般不確定(當.net認為記憶體緊張時﹐它就會開始)

BTW:其實我們就是想自己收回物件的記憶體也不可能﹐因為MS沒有提供途徑(GC.Collect也是啟動.net的記憶體收集功能)

五.第一個結論
在net中使用物件很簡單﹐建立物件之後直接使用就可以了﹐不用了也不要去管它﹐垃圾收集器會幫你把記憶體要回來的。

六.例外
當物件的成員引用了一個非託管資源時(不在託管堆上分配的記憶體或資源﹐像檔案﹐資料庫連線等等)﹐下面以一個例子來說明﹕
System.IO.FileStream類別﹐這是.net基本類庫提供的一個非託管資源(檔案)封裝物件(用Reflector工具反編譯mscorlib.dll可見其程式碼)

1.FileStream毫無疑問封裝了一個非託管資源

觀其原始碼發現有這樣一個私有成員﹕

private SafeFileHandle _handle;

 通過構造器呼叫的Init方法可以發現這個成員的初始化程式碼﹕

this._handle = Win32Native.SafeCreateFile(text2, num1, share, secAttrs, mode, num2,  
Win32Native.NULL);

 而後者實際上就是kernel32.dll中的CreateFile方法﹐它返回一個HANDLE(即非託管資源引用)

2.我們先來使用這個類別﹕

using System;
 
using System.IO;
 
 
public class TestFileStream
 
{
    
public static void Main(string[] args)
    
{    
          
//建立一個FileStream物件
        FileStream fs = new FileStream(@"C: est.txt",FileMode.OpenOrCreate);        
       Console.WriteLine(
"您可以嘗試在系統中刪除c盤下的test.txt(Enter鍵繼續)");
        
//暫停程式執行﹐並嘗試在系統中刪除那個檔案
        Console.ReadLine();

      
//刪除檔案測試
       try
       
{
          File.Delete(
@"c: est.txt");
      }

       
catch (IOException ex)
       
{
           Console.WriteLine(
"[Error]程式刪除檔案失敗﹕{0}",ex.Message);
       }

    }

}
3.在程式掛起時(Console.ReadLine等待輸入)﹐刪除檔案會失敗﹐很容易理解﹐因為檔案開啟後沒有將其關閉﹐系統不知道這個檔案是否還有用﹐所以幫我們保護這個檔案(理所當然﹐那個非託管資源所使用的記憶體還被程式佔用著)

4.但是在程式執行完後﹐我們再嘗試刪除檔案﹐成功﹗為什麼?(fs不是沒有關閉那個SafeFileHandle嗎?)
當然您可以說﹐windows作業系統在一個程式結束後會自動回收其資源﹐沒錯(但是如果是com就慘了﹐因為com是存在於自己的獨立程式內﹐而作業系統不負責這個:(   )﹐不過這裡不是因為windows作業系統的功能﹐而是.net垃圾收集器幫的忙。

5.看下面這個例子

 using System;
 
using System.IO;

 
public class TestFileStream
 
{
   
public static void Main(string[] args)
  
{
      
//建立一個FileStream物件
      FileStream fs = new FileStream(@"C: est.txt", FileMode.OpenOrCreate);
      Console.WriteLine(
"您可以嘗試在系統中刪除c盤下的test.txt(Enter鍵繼續)");
      
//暫停程式執行﹐並嘗試在系統中刪除那個檔案
       Console.ReadLine();

        
/**//*進行垃圾收集*/
      GC.Collect();
      Console.WriteLine(
"再刪一下試試");
       Console.ReadLine();
   }

}
6.注意中間那行程式碼:
GC.Collect();

這是強制要.net垃圾收集器進行垃圾收集。
我們再去嘗試刪除test.txt﹐居然可以被刪除了﹐為什麼呀?(fs不是沒有關閉那個SafeFileHandle嗎?)﹐讓我細細道來﹕

7.我們首先了解一下.net垃圾收集器進行垃圾收集的四種時機(參見﹕.net框架程式設計 李建忠譯)
a.最常見的﹕當.net覺得合適時﹐例如它感到記憶體緊張了(朮語稱為﹕0代物件充滿)
b.微軟強烈不建議使用的﹕GC的Collect方法呼叫(就是我們上面用的這種啦﹐因為會降低效能﹐會掛起程式, 等等﹐反正聽微軟的吧。當然某些時候可以用﹐就像我上面用來測試的程式碼﹐呵呵...)
c.應用程式域解除安裝時(AppDomain)
d.CLR被關閉時

8.現在我們可以明白第1個例子為什麼在程式結束後檔案可以被刪除﹐因為CLR被關閉時﹐.net執行了垃圾收集(也就是等同於第二個例子的GC.Collect()程式碼)

9.所以現在所有的問題都集中到垃圾收集上面﹐它做了什麼?

a.垃圾收集器在判斷一個物件不會再被引用到後﹐就開始對它進行垃圾收集(即回收記憶體)
b.清空記憶體(即把託管堆中的記憶體收回來)
c.但是物件的有些欄位引用到了非託管資源怎麼辦?如FileStream的_handle
d.所以我們必須告訴垃圾收集器﹐在你回收我的記憶體之前﹐先幫我執行一個方法來收回我的非託管資源﹐以免託管堆的記憶體被你回收了﹐而我引用的非託管資源的記憶體卻被洩漏了。
e.這個方法就是Finalize()﹐也就是C#的 ~ClassName() 方法(同C++中的析構語法)
解構函式是在類名前加~.也沒有返回值. ,不過C#的解構函式的呼叫機制和C++不同.並不能保證每次都會呼叫.所以最好不要利用C#的解構函式來回收資源. C#中解構函式沒有什麼意義 因為C#是託管程式 何時析構由系統作出判斷,執行垃圾回收

f.所以一個物件如果存在Finalize方法時﹐垃圾收集器在收回它的記憶體之前就會自動呼叫這個方法
g.這樣我們就可以把那些東東(非託管資源)給清理乾淨了

由此看來﹐垃圾收集器提供的這種機制就是為了更好的完善.net的自動記憶體管理的功能﹐讓我們也可以參與到垃圾收集中去

10.我們再來看看GC.Collect()這行程式碼或CLR關閉時.Net做了什麼﹕
a.垃圾收集器啟動﹐發現fs引用的那個物件已經沒用了(當然CLR關閉時才不管你有沒有用﹐通通回收)﹐於是對它進行記憶體回收
b.發現fs的型別﹕FileStream提供了Finalize方法﹐於是先呼叫這個方法
(以下通過Reflector繼續)
c.Finalize方法中有 this._handle.Dispose()程式碼﹐於是呼叫SafeHandler.Dispose()
d.接著轉到(當然好多個圈﹐您悠著點...)SafeFileHandle.ReleaseHandle方法﹐發現程式碼﹕Win32Native.CloseHandle() (即關閉非託管資源--檔案HANDLE)

真相大白﹕原來是垃圾收集器幫我們關閉了那個非託管資源(當然還是通過我們自己寫的Finalize方法)﹐因此後面就可以刪除檔案了。

11.有人會問﹕好像我們平時在使用FileStream物件時﹐沒這麼複雜呀?
答﹕Very Good!

一部分人﹕是因為大家都和我的例1一樣有好運氣﹐那個C盤下的test.txt檔案自從被建立後﹐我壓根就不會再去用它﹐管它這部分資源有沒有被洩漏﹐有沒有被鎖定﹐最後程式結束時﹐被垃圾收集器幫了忙﹐把忘了關閉的檔案HANDLE給收回來了。

剩下的一部分人﹕在程式裡埋下了一顆"啞彈"﹐不知什麼時候會爆炸﹐就像我例子中的File.Delete方法就出現了異常。

(不過我覺得)絕大多數人﹕是在看了很多諸如.net程式設計忠告﹐Microsoft強烈建議﹐MSDN標準做法等等等等( 還有我這篇blog﹐呵呵)之後﹐知道了在使用如FileStream,SqlConnection這些東東時﹐必須將其Close。

12.Close與Dispose
檢視我們那兩個例子的程式碼﹐都是不標準的﹐正確做法應該在使用完那個FileStream後﹐呼叫fs.Close()將其關閉﹐以保證資源的安全。

附﹕正確做法

using System;
 
using System.IO;

 
public class TestFileStream
 
{
   
public static void Main(string[] args)
    
{
         
//建立一個FileStream物件
         FileStream fs = new FileStream(@"C: est.txt", FileMode.OpenOrCreate);

        
/**//*在用完FileStream後關閉*/
       fs.Close();
       
//刪除檔案測試
      try
      
{
           File.Delete(
@"c: est.txt");
       }

       
catch (IOException ex)
       
{
            Console.WriteLine(
"[Error]程式刪除檔案失敗﹕{0}", ex.Message);
       }

}

13.有人舉手﹐講這麼多﹐早告訴我呼叫fs.Close不就得了。
哥們﹐fs.Close()方法是由您寫的﹐調不呼叫﹐手在您身上﹐您不呼叫的話﹐哪天程式出了問題﹐您有會叫﹕微軟真垃圾﹐.net真不穩定﹐還是java好﹐安全﹐可靠...    為防您的國罵﹐MS只好在垃圾收集中加這一款﹐以防不測...

14.Dispose模式
認真檢視.net類庫中的那些基本類別﹐凡是有Finalize方法的類別﹐基本上都提供了諸如Dispose,Close,Dispose(bool)等方法(FileStream也不例外)

15.其實不管是Dispose,Close,Finalize方法﹐最終應該都是執行相同的程式碼
區別﹕
Finalize方法﹕只能由微軟呼叫
Dispose和Close方法﹕提供給您呼叫
因此在您使用完那些類別後﹐那就直接呼叫Close吧(沒有Close﹐再呼叫Dispose方法)﹐當然萬一您忘了﹐也別擔心﹐還有垃圾收集器幫您墊後。

七.第二個結論﹕
1.在您開發一個封裝非託管資源(即類中的欄位引用到了非託管資源)的類別時﹕
A:強烈建議您提供Finalize方法進行非託管資源的釋放﹐.net垃圾收集器不會幫您自動回收那部分資源﹐而是通過呼叫您的Finalize方法來幫您釋放。(這樣可以保證﹕在使用您類別的那位程式設計師忘了手動回收記憶體時﹐還可通過垃圾收集器來補救)

B.強烈建議您提供一個Close或Dispose方法﹐以便使用您類別的程式設計師可以手動釋放您的類別中的非託管資源。(參見.net框架程式設計 自動記憶體管理一章實現Dispose模式)

C.如果類別封裝了像FileStream這樣的物件(即對非託管資源的再次封裝)時﹐一般也應該提供一 個Close或Dispose方法﹐除非您的這個成員保證在每次使用後﹐都被正常的關閉﹐即對呼叫者透明。

2.在您使用一個封裝非託管資源的類別時﹕
A:強烈建議您在明確知道這個類別沒有用之後﹐呼叫其提供的Close或Dispose方法手動釋放其非託管資源的 記憶體。有道是﹕有借有還﹐再借不難;借了不還﹐再借休想~~

B:注意在手動釋放後﹐不要再呼叫該物件的相關方法了﹐因為物件已經損毀了

再次BTW:不管是Finalize﹐Close還是Dispose﹐您都無法顯式釋放託管堆記憶體﹐它們永遠是微軟的"私人財產 "﹕)

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-611627/,如需轉載,請註明出處,否則將追究法律責任。

相關文章