前面一篇文章介紹了垃圾回收的基本工作原理,垃圾回收器并不是可以管理內(nèi)存中的所有資源。對(duì)于所有的托管資源都將有.NET垃圾回收機(jī)制來(lái)釋放,但是,對(duì)于一些非托管資源,我們就需要自己編寫(xiě)代碼來(lái)清理這類(lèi)資源了。
其實(shí)在C#開(kāi)發(fā)中,大部分資源都可以通過(guò).NET垃圾回收機(jī)制進(jìn)行回收,只用當(dāng)我們使用非托管資源(原始的操作系統(tǒng)文件句柄,原始的非托管數(shù)據(jù)庫(kù)連接,非托管內(nèi)存等等)的時(shí)候,我們才需要實(shí)現(xiàn)自己的資源清理代碼。
.NET提供了兩種釋放非托管資源的方式,類(lèi)型自己的Finalize方法和IDisposable接口的Dispose方法。
下面就來(lái)看看這兩個(gè)跟垃圾回收相關(guān)的方法。
Finalize方法
在.NET的基類(lèi)System.Object中,定義了名為Finalize()的虛方法,這個(gè)方法默認(rèn)什么都不做。
我們可以為自定義的類(lèi)型重寫(xiě)Finalize方法,在該方法中加入必要的非托管資源清理邏輯。當(dāng)要從內(nèi)存中刪除這個(gè)類(lèi)型的對(duì)象時(shí),垃圾回收器會(huì)調(diào)用對(duì)象的Finalize方法。所以,無(wú)論.NET進(jìn)行一次自發(fā)的垃圾回收,還是我們通過(guò)GC.Collect()進(jìn)行強(qiáng)制垃圾回收,F(xiàn)inalize方法總是會(huì)被調(diào)用。另外,當(dāng)承載應(yīng)用程序的AppDomain從內(nèi)存中移除時(shí),同樣會(huì)調(diào)用Finalize方法。
重寫(xiě)Finalize方法
假設(shè)我們現(xiàn)在有一個(gè)使用非托管資源的類(lèi)型,那么我們就需要重寫(xiě)Finalize方法來(lái)進(jìn)行非托管資源的清理,但是當(dāng)通過(guò)下面的方式重寫(xiě)Finalize方法的時(shí)候,我們會(huì)得到一個(gè)編譯錯(cuò)誤。
class MyResourceWrapper
{
protected override void Finalize()
{
}
}
其實(shí),當(dāng)我們想要重寫(xiě)Finalize方法時(shí),C#為我們提供了(類(lèi)似C++)析構(gòu)函數(shù)語(yǔ)法(C#終結(jié)器)來(lái)重寫(xiě)該方法。C#終結(jié)器和構(gòu)造函數(shù)語(yǔ)法類(lèi)似,方法名稱(chēng)都和類(lèi)型名稱(chēng)一樣;不同的是,終結(jié)器具有~前綴,并且不能使用訪(fǎng)問(wèn)修飾符,不接受參數(shù),也不能重載,所以一個(gè)類(lèi)只能有一個(gè)終結(jié)器。
class MyResourceWrapper
{
~MyResourceWrapper()
{
Console.WriteLine("release unmanaged resources");
Console.Beep();
}
}
之所以C#只支持這種方式進(jìn)行Finalize方法的重寫(xiě),是因?yàn)镃#編譯器會(huì)為Finalize方法隱式地加入一些必需的基礎(chǔ)代碼。下面就是我們通過(guò)ILSpy查看到了IL代碼,F(xiàn)inalize方法作用域內(nèi)的代碼被放在了一個(gè)try塊中,然后不管在try塊中是否遇到異常,finally塊保證了Finalize方法總是能夠被執(zhí)行。
.method family hidebysig virtual
instance void Finalize () cil managed
{
// Method begins at RVA 0x2050
// Code size 31 (0x1f)
.maxstack 1
.try
{
IL_0000: nop
IL_0001: ldstr "release unmanaged resources"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: call void [mscorlib]System.Console::Beep()
IL_0011: nop
IL_0012: nop
IL_0013: leave.s IL_001d
} // end .try
finally
{
IL_0015: ldarg.0
IL_0016: call instance void [mscorlib]System.Object::Finalize()
IL_001b: nop
IL_001c: endfinally
} // end handler
IL_001d: nop
IL_001e: ret
} // end of method MyResourceWrapper::Finalize
當(dāng)我們執(zhí)行下面代碼時(shí),我們就可以聽(tīng)到系統(tǒng)蜂鳴聲,像我們前面介紹的一樣AppDomain被移除內(nèi)存,類(lèi)型終結(jié)器將被調(diào)用。
static void Main(string[] args)
{
MyResourceWrapper mr = new MyResourceWrapper();
}
Finalize的工作機(jī)制
Finalize的工作機(jī)制還是比較復(fù)雜的,這里只是簡(jiǎn)單的介紹,更多的原理大家可以自己網(wǎng)上查查。
當(dāng)在托管堆上分配對(duì)象空間時(shí),運(yùn)行庫(kù)會(huì)自動(dòng)確定該對(duì)象是否提供一個(gè)自定義的Finalize方法。如果是這樣,對(duì)象被標(biāo)記為可終結(jié)的,同時(shí)一個(gè)指向這個(gè)對(duì)象的指針被保存在名為終結(jié)隊(duì)列的內(nèi)部隊(duì)列中。終結(jié)隊(duì)列是一個(gè)由垃圾回收器維護(hù)的表,它指向每一個(gè)在從堆上刪除之前必須終結(jié)的對(duì)象。
當(dāng)垃圾回收器確定到了從內(nèi)存中釋放一個(gè)對(duì)象的時(shí)間時(shí),它檢查終結(jié)隊(duì)列上的每一個(gè)項(xiàng),并將對(duì)象從堆上復(fù)制到另一個(gè)稱(chēng)作終結(jié)可達(dá)表(finalization reachable table的托管結(jié)構(gòu)上。此時(shí),下一個(gè)垃圾回收時(shí)將產(chǎn)生另外一個(gè)線(xiàn)程,為每一個(gè)在可達(dá)表中的對(duì)象調(diào)用Finalize方法。因此,為了真正終結(jié)一個(gè)對(duì)象,至少要進(jìn)行兩次垃圾回收。
從上面可以看到,F(xiàn)inalize方法的調(diào)用是相當(dāng)消耗資源的。Finalize方法的作用是保證.NET對(duì)象能夠在垃圾回收時(shí)清理非托管資源,如果創(chuàng)建了一個(gè)不使用非托管資源的類(lèi)型,實(shí)現(xiàn)終結(jié)器是沒(méi)有任何作用的。所以說(shuō),如果沒(méi)有特殊的需求應(yīng)該避免重寫(xiě)Finalize方法。
IDisposable接口
當(dāng)垃圾回收生效時(shí),可以利用終結(jié)器來(lái)釋放非托管資源。然而,很多非托管資源都非常寶貴(如數(shù)據(jù)庫(kù)和文件句柄),所以它們應(yīng)該盡可能快的被清除,而不能依靠垃圾回收的發(fā)生。除了重寫(xiě)Finalize之外,類(lèi)還可以實(shí)現(xiàn)IDisposable接口,然后在代碼中主動(dòng)調(diào)用Dispose方法來(lái)釋放資源。
看一個(gè)例子:
class MyResourceWrapper:IDisposable
{
public void Dispose()
{
Console.WriteLine("release resources with Dispose");
Console.Beep();
}
}
class Program
{
static void Main(string[] args)
{
MyResourceWrapper mr = new MyResourceWrapper();
mr.Dispose();
}
}
同樣,當(dāng)我們顯示的調(diào)用Dispose方法的時(shí)候,可以聽(tīng)到系統(tǒng)的蜂鳴聲。
注意,通過(guò)Dispose進(jìn)行資源的釋放也是有潛在的風(fēng)險(xiǎn)的,因?yàn)镈ispose方法需要被程序員顯示的調(diào)用,如果代碼中漏掉了Dispose的調(diào)用或者在Dispose調(diào)用之前產(chǎn)生了異常從而沒(méi)有指定Dispose,那么有些資源可能就一直留在內(nèi)存中了。
所以我們應(yīng)該使用下面的方式保證Dispose方法可以被調(diào)用到:
static void Main(string[] args)
{
MyResourceWrapper mr = new MyResourceWrapper();
try
{
//do something wiht mr object
}
finally
{
mr.Dispose();
}
}
但是,每次編寫(xiě)Dispose的代碼都使用try塊會(huì)覺(jué)得很麻煩,還好C#中,我們可以重用using關(guān)鍵字來(lái)簡(jiǎn)化Dispose的調(diào)用。
重用using關(guān)鍵字
在C#中,using語(yǔ)句提供了一個(gè)高效的調(diào)用對(duì)象Dispose方法的方式。對(duì)于任何IDispose接口的類(lèi)型,都可以使用using語(yǔ)句,而對(duì)于那些沒(méi)有實(shí)現(xiàn)IDisposable接口的類(lèi)型,使用using語(yǔ)句會(huì)導(dǎo)致一個(gè)編譯錯(cuò)誤。
static void Main(string[] args)
{
using (MyResourceWrapper mr = new MyResourceWrapper())
{
//do something with mr object
}
}
在using語(yǔ)句塊結(jié)束的時(shí)候,mr實(shí)例的Dispose方法將會(huì)被自動(dòng)調(diào)用。using語(yǔ)句不僅免除了程序員輸入Dispose調(diào)用的代碼,它還保證Dispose方法被調(diào)用,無(wú)論using語(yǔ)句塊順利執(zhí)行結(jié)束,還是拋出一個(gè)異常。事實(shí)上,C#編譯器為using語(yǔ)句自動(dòng)添加了try/finally塊。我們可以看看using的IL代碼:
.try
{
IL_0007: nop
IL_0008: nop
IL_0009: leave.s IL_001b
} // end .try
finally
{
IL_000b: ldloc.0
IL_000c: ldnull
IL_000d: ceq
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: brtrue.s IL_001a
IL_0013: ldloc.0
IL_0014: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0019: nop
IL_001a: endfinally
} // end handler
Dispose和Finalize的結(jié)合
從前面的介紹了解到,F(xiàn)inalize可以通過(guò)垃圾回收進(jìn)行自動(dòng)的調(diào)用,而Dispose需要被代碼顯示的調(diào)用,所以,為了保險(xiǎn)起見(jiàn),對(duì)于一些非托管資源,還是有必要實(shí)現(xiàn)終結(jié)器的。也就是說(shuō),如果我們忘記了顯示的調(diào)用Dispose,那么垃圾回收也會(huì)調(diào)用Finalize,從而保證非托管資源的回收。
其實(shí),MSDN上給我們提供了一種很好的模式來(lái)實(shí)現(xiàn)IDisposable接口來(lái)結(jié)合Dispose和Finalize,例如下面的代碼:
class MyResourceWrapper:IDisposable
{
private bool IsDisposed=false;
public void Dispose()
{
Dispose(true);
//tell GC not invoke Finalize method
GC.SuppressFinalize(this);
}
protected void Dispose(bool Disposing)
{
if(!IsDisposed)
{
if(Disposing)
{
//clear managed resources
}
//clear unmanaged resources
}
IsDisposed=true;
}
~MyResourceWrapper()
{
Dispose(false);
}
}
在這個(gè)模式中,void Dispose(bool Disposing)函數(shù)通過(guò)一個(gè)Disposing參數(shù)來(lái)區(qū)別當(dāng)前是否是被Dispose()調(diào)用。如果是被Dispose()調(diào)用,那么需要同時(shí)釋放托管和非托管的資源。如果是被終結(jié)器調(diào)用了,那么只需要釋放非托管的資源即可。Dispose()函數(shù)是被其它代碼顯式調(diào)用并要求釋放資源的,而Finalize是被GC調(diào)用的。
另外,由于在Dispose()中已經(jīng)釋放了托管和非托管的資源,因此在對(duì)象被GC回收時(shí)再次調(diào)用Finalize是沒(méi)有必要的,所以在Dispose()中調(diào)用GC.SuppressFinalize(this)避免重復(fù)調(diào)用Finalize。同樣,因?yàn)镮sDisposed變量的存在,資源只會(huì)被釋放一次,多余的調(diào)用會(huì)被忽略。
所以這個(gè)模式的優(yōu)點(diǎn)可以總結(jié)為:
如果沒(méi)有顯示的調(diào)用Dispose(),未釋放托管和非托管資源,那么在垃圾回收時(shí),還會(huì)執(zhí)行Finalize(),釋放非托管資源,同時(shí)GC會(huì)釋放托管資源
如果調(diào)用了Dispose(),就能及時(shí)釋放了托管和非托管資源,那么該對(duì)象被垃圾回收時(shí),就不會(huì)執(zhí)行Finalize(),提高了非托管資源的使用效率并提升了系統(tǒng)性能
總結(jié)
本文介紹了.NET垃圾回收中兩個(gè)相關(guān)的方法:Dispose和Finalize。Finalize的目的是用于釋放非托管的資源,而Dispose是用于釋放所有資源,包括托管的和非托管的。
Dispose需要在代碼中進(jìn)行顯示的調(diào)用,而Finalize則是由垃圾回收自動(dòng)調(diào)用,為了更有效的結(jié)合Dispose和Finalize,文中還介紹了MSDN中給出的實(shí)現(xiàn)IDisposable接口的一個(gè)模式。
更多信息請(qǐng)查看IT技術(shù)專(zhuān)欄