淺談C#的垃圾回收----關於GC、解構函式、Dispose、and Finalize
對於.Net CLR的垃圾自動回收,這兩日有興致小小研究了一下。查閱資料,寫程式碼測試,發現不研究還罷,越研究越不明白了。在這裡sban寫下自己的心得以拋磚引玉,望各路高手多多指教。
近日瀏覽Msdn2,有一段很是費解,引於此處:
原文:http://msdn2.microsoft.com/zh-cn/library/0s71x931(VS.80).aspx
英文:把zh-cn替成en-us。此文件對應.net2.0,把VS.80替成VS.90可檢視.net3.5最新文件。兩者無甚差別,可見自.net1.1之後,垃圾回收機制沒有改變。
據上文引用,關於GC的二次回收,sban作圖一張,如下:
為了驗證GC對含有終結器物件的兩次回收機制,我寫了一個例子測試,程式碼如下:
using System.Threading;
using System.IO;
using System.Data.SqlClient;
namespace Lab
{
class Log
{
public static readonly string logFilePath = @"d:/log.txt";
public static void Write(string s)
{
Thread.Sleep(10);
using (StreamWriter sw = File.AppendText(logFilePath))
//此處有可能丟擲檔案正在使用的異常,但不影響測試
{
sw.WriteLine(
DateTime.Now.TimeOfDay.TotalMilliseconds, GC.GetTotalMemory(false));
sw.Close();
}
}
}
class World
{
protected FileStream fs = null;
protected SqlConnection conn = null;
public World()
{
fs = new FileStream(Log.logFilePath, FileMode.Open);
conn = new SqlConnection();
}
protectedvoid Finalize()
{
fs.Dispose();
conn.Dispose();
Log.Write("World's destructor is called");
}
}
class China : World
{
public China()
: base()
{
}
~China()
{
Log.Write("China's destructor is called");
}
}
class Beijing : China
{
public Beijing()
: base()
{
}
~Beijing()
{
Log.Write("Beijing's destructor is called");
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
namespace Lab
{
class Program
{
static void Main(string[] args)
{
TestOne();
Log.Write("In Main../t/t");
}
static void TestOne()
{
new Beijing();
GC.Collect();
GC.WaitForPendingFinalizers();
Log.Write("In TestOne../t/t");
}
}
}
F5執行一下,返回如下結果:
In Main.. TotalMilliseconds:53009887.496 TotalMemory:783056
Beijing's destructor is called TotalMilliseconds:53589020.248 TotalMemory:697744
China's destructor is called TotalMilliseconds:53589030.2624 TotalMemory:714128
World's destructor is called TotalMilliseconds:53589040.2768 TotalMemory:738704
In TestOne.. TotalMilliseconds:53589050.2912 TotalMemory:763280
In Main.. TotalMilliseconds:53589060.3056 TotalMemory:779664
注:重點看時間與記憶體變化,以下同。
WaitForPendingFinalizers()相當於join了終結器佇列執行執行緒。派生類Beijing及其父類的終結器確實已經成功執行,但是為什麼記憶體佔用卻不降反升?結合兩次結果來看,垃圾回收器真正釋放記憶體應該是在退出當前應用程式域之後發生的。msdn2中有云:
看來msdn此言非虛
但是為什麼記憶體沒有真正被GC回收呢?World的終結器既已執行,其中fs.Dispose()與conn.Dispose()也得以成功執行,為什麼就連微軟鼓勵使用的Dispose()也不好使了呢?是fs與conn物件不佔記憶體,差別微乎其微嗎?為了驗證是與不是,把上文例碼中的fs與conn的相關定義及初始化程式碼一併去掉。再執行一下:
China's destructor is called TotalMilliseconds:54514100.448 TotalMemory:582508
World's destructor is called TotalMilliseconds:54514110.4624 TotalMemory:598892
In TestOne.. TotalMilliseconds:54514120.4768 TotalMemory:623468
In Main.. TotalMilliseconds:54514130.4912 TotalMemory:639852
Beijing's destructor is called TotalMilliseconds:56343741.3424 TotalMemory:563252
China's destructor is called TotalMilliseconds:56343751.3568 TotalMemory:579636
World's destructor is called TotalMilliseconds:56343761.3712 TotalMemory:596020
In TestOne.. TotalMilliseconds:56343771.3856 TotalMemory:620596
In Main.. TotalMilliseconds:56343781.4 TotalMemory:636980
記憶體佔用明顯減少,看樣子沒有冤枉GC。讓它回收,它確實沒有給我幹活啊。
在C#中,如果一個自定義類沒有構造器,編譯器會新增一個隱藏的無參構造器。但是解構函式不會自動建立。一旦解構函式建立了,終結器也便自動產生了。構構函式其實等同於如下程式碼:
Finalize();
}finally{
base.Finalize();
}
在上文程式碼中,World類雖沒有解構函式,也被派生類China的析構觸發得以執行。
如果在派生類中不存在析造函式,卻過載了基類的終結器,如下:
垃圾回收時,GC找不到建構函式,會直接呼叫終結器。因終結器已重寫,如果在該終結器中不得呼叫基類的終結器,那麼GC將忽略基類。可以利用這個特性寫一個不受垃圾回收器管轄的類,以實現某種特殊的效果。此乃旁邊左道,與高手見笑了。
對於上文程式碼,如果把TestOne函式改成如下:
{
Beijing bj = new Beijing();
GC.Collect();
Log.Write("In TestOne../t/t");
}
執行一下,GC貌似無用,bj及其父類的解構函式依然在Log.Write("In TestOne...")之後執行,有無WaitForPendingFinalizers()無甚差別。但如果只new一下物件,並不賦值於變數,code如下:
static void TestOne(){
new Beijing();
GC.Collect();
Log.Write("In TestOne../t/t");
}
執行結果如下:
In TestOne.. TotalMilliseconds:59773883.6448 TotalMemory:586612
China's destructor is called TotalMilliseconds:59773893.6592 TotalMemory:602996
In Main.. TotalMilliseconds:59773893.6592 TotalMemory:619380
World's destructor is called TotalMilliseconds:59773903.6736 TotalMemory:635764
Beijing's destructor is called TotalMilliseconds:59775696.2512 TotalMemory:561080
China's destructor is called TotalMilliseconds:59775706.2656 TotalMemory:577464
In TestOne.. TotalMilliseconds:59775706.2656 TotalMemory:602040
World's destructor is called TotalMilliseconds:59775716.28 TotalMemory:618424
In Main..
不用WaitForPendingFinalizers()也觸發了Beijing及其父類的解構函式。GC.Collect是非同步呼叫,該問一代而過。至於Log.Write執行的先後,要看誰能獲得log檔案操作控制代碼。上文寫log檔案的程式碼有問題,多執行緒應用中可能引發檔案正在使用的異常,實際應用中應先申請檔案控制代碼,申請成功lock之後方可操作。由於Write方法中讓執行緒沉睡了10毫秒,故GC在此空檔內有機會獲得了檔案操作控制代碼。
有兩種情況GC都是可以觸發物件的解構函式的:
1,如前面所說,在退出當前應用程式域時。
2,當物件不能再被訪問時。若只是new一個物件,轉行便滿足條件。
對於GC.Collect,有兩個版本:
1,GC.Collect();
2,GC.Collect(int32);引數為Generatio。什麼是Generation?
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
namespace Lab
{
class Program
{
static void Main(string[] args)
{
TestObject obj = new TestObject();
int generation = 0;
for (int j = 0; j < 6; j++)
{
generation = GC.GetGeneration(obj);
Console.WriteLine(j.ToString());
Console.WriteLine("TotalMemory:{0}", GC.GetTotalMemory(false));
Console.WriteLine("MaxGeneration:{0}", GC.MaxGeneration);
Console.WriteLine("Value:{0},String:{1}", obj.Value, obj.String.Length);
Console.WriteLine("Generation:{0}", generation);
Console.WriteLine();
GC.Collect();
GC.WaitForPendingFinalizers();
}
Console.Read();
}
class TestObject
{
public int Value = 0;
public string String = "0";
public TestObject()
{
for (int j = 0; j < 100; j++)
{
Value++;
String += j.ToString();
}
}
}
}
}
執行一個,結果如下:
GC回收記憶體從0代開始,打掃0代中所有可以清除的物件。暫時不可清除的物件移到1代中。依此類推,清除1代物件時,尚用物件則移至2代。第一次回收之後,可回收記憶體空間已經很小,回收效果已不明顯。故平常強制垃圾回收用函式GC.Collect()不如用GC.Collect(0)。
{
private function GC(){};
public static function Collect():void
{
try{
new LocalConnection .connect("GC1");
new LocalConnection .connect("GC2");
}catch(e:*){}
}
}
對於比較耗費資源的物件,如LocalConnection,如果它們丟擲異常,一般垃圾回收器不會坐視不理。那麼,這個不怎麼正宗的方法在.Net也可以嗎?答案是肯定的。在.Net中,如果檔案控制代碼、資料庫連線等物件操作出錯時,GC會嘗試強制回收記憶體。修改上文Main函式程式碼如下,以作測試: static void Main(string[] args)
{
TestObject obj = new TestObject();
int generation = 0;
generation = GC.GetGeneration(obj);
Console.WriteLine(0);
Console.WriteLine("TotalMemory:{0}", GC.GetTotalMemory(false));
Console.WriteLine("MaxGeneration:{0}", GC.MaxGeneration);
Console.WriteLine("Value:{0},String:{1}", obj.Value, obj.String.Length);
Console.WriteLine("Generation:{0}", generation);
Console.WriteLine();
try
{
new SqlConnection("Null").Open();
}
catch (Exception e) { }
for (int j = 1; j < 6; j++)
{
generation = GC.GetGeneration(obj);
Console.WriteLine(j.ToString());
Console.WriteLine("TotalMemory:{0}", GC.GetTotalMemory(false));
Console.WriteLine("MaxGeneration:{0}", GC.MaxGeneration);
Console.WriteLine("Value:{0},String:{1}", obj.Value, obj.String.Length);
Console.WriteLine("Generation:{0}", generation);
Console.WriteLine();
GC.Collect();
GC.WaitForPendingFinalizers();
}
Console.Read();
}
執行一下,結果如圖所示: 可見,SqlConnection丟擲異常時,GC果真進行了回收。再執行一下,結果卻變了: 唏!怎麼沒有回收,記憶體反而升高了。可見,GC確實有點智慧,第一次回收了,第二次似乎做了點別的動作,致使記憶體反而升高。Msdn2中有云,GC自己可以確定回收垃圾的最好時機與方法,所以奉勸使用者一般不要手動干預,不然可能會南轅北轍。 那.Net程式設計師在程式設計時應該怎麼做,有沒有一種既簡單又有有效的方法來處理內在回收。愚人作以下建議,望各路高手不吝賜教: 1,對於不包涵或沒有引用(直接或間接)非託管資源的類,特別是作用如同Struct的實體類,析構、終結器、Dispose均不採用。 2,對於包涵非託管資源的類,如資料庫連線物件,檔案控制代碼等,應繼承IDispose介面,在Dispose方法中清理非託管物件。客戶程式碼用using(…){}格式顯示呼叫Dispose。如果繼承了IDispose介面,Dispose方法就不要留空,這樣沒有任何意義。除了構造器,任何方法體留空都有害無益。 3,所有自定義類一般均不建議顯式宣告解構函式、Finalize方法。
文章:淺談C#的記憶體回收----關於GC、解構函式、Dispose、and Finalize
作者:sban 2007/12/1首發於部落格園