釋放類所使用的未託管資源的兩種方式:
1.利用運行庫強制執行的析構函數,但析構函數的執行是不確定的,而且,由於垃圾收集器的工作方式,它會給運行庫增加不可接受的系統開銷。
2.IDisposable接口提供了一種機制,允許類的用戶控制釋放資源的時間,但需要確保執行Dispose()。
一般情況下,最好的方法是執行這兩種機制,獲得這兩種機制的優點,克服其缺點。假定大多數程序員都能正確調用Dispose(),實現IDisposable接口,同時把析構函數作爲一種安全的機制,以防沒有調用Dispose()。
ispose()有第二個protected重載方法,它帶一個bool參數,這是真正完成清理工作的方法。Dispose(bool)由析構函數和IDisposable.Dispose()調用。這個方式的重點是確保所有的清理代碼都放在一個地方。
傳遞給Dispose(bool)的參數表示Dispose(bool)是由析構函數調用,還是由IDisposable.Dispose()調用——Dispose(bool)不應從代碼的其他地方調用,其原因是:
● 如果客戶調用IDisposable.Dispose(),該客戶就指定應清理所有與該對象相關的資源,包括託管和非託管的資源。
● 如果調用了析構函數,在原則上,所有的資源仍需要清理。但是在這種情況下,析構函數必須由垃圾收集器調用,而且不應訪問其他託管的對象,因爲我們不再能確定它們的狀態了。在這種情況下,最好清理已知的未託管資源,希望引用的託管對象還有析構函數,執行自己的清理過程。
isDispose成員變量表示對象是否已被刪除,並允許確保不多次刪除成員變量。這個簡單的方法不是線程安全的,需要調用者確保在同一時刻只有一個線程調用方法。要求客戶進行同步是一個合理的假定,在整個.NET類庫中反覆使用了這個假定(例如在集合類中)。
最後,IDisposable.Dispose()包含一個對System.GC. SuppressFinalize()方法的調用。SuppressFinalize()方法則告訴垃圾收集器有一個類不再需要調用其析構函數了。因爲Dispose()已經完成了所有需要的清理工作,所以析構函數不需要做任何工作。調用SuppressFinalize()就意味着垃圾收集器認爲這個對象根本沒有析構函數
<span style="font-size:14px;">public class ResourceHolder : IDisposable
{
private bool isDispose = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
//釋放託管資源
}
//釋放非託管資源
}
isDisposed=true;
}
~ResourceHolder()
{
Dispose(false);
}
} </span>
1.被分配內存空間的對象最有可能被釋放。在方法執行時,就需要爲該方法的對象分配內存空間,搜索最近分配的對象集合有助於花費最少的代價來儘可能多地釋放內存空間。
2.生命期最長的對象釋放的可能性最小,經過幾輪垃圾回收後,對象仍然存在,搜索它時就需要進行大量的工作,卻只能釋放很小的一部分空間。
3.同時被分配內存的對象通常是同時使用,將它們彼此相連有助於提高緩存性能和回收效率。
C#中的回收器是分代的垃圾回收器(Gererational Garbage Collector) 它將分配的對象分爲3個類別或代。(可用GC.GetGeneration方法返回任意作爲參數的對象當前所處的代)最近被分配內存的對象被放置於第0 代,因爲第0代很小,小到足以放進處理器的二級(L2)緩存,所以它能夠提供對對象的快速存取。經過一輪垃圾回收後,仍然保留在第0代中的對象被移進第1 代中,再經過一輪垃圾內存回收後,仍然保留在第1代中的對象則被移進第2代中,第2代中包含了生存期較長的對象。(類比於JAVA中的分代收集器)
在C#中值類型是在堆棧中分配內存,它們有自身的生命週期,所以不用對它們進行管理,會自動分配和釋放。而引用類型是在堆中分配內存的。所以它的分配和釋放就需要像回收機制來管理。C#爲一個對象分配內存時,託管堆可以立即返回新對象所需的內存,因爲託管堆類似於簡單的字節數組,有一個指向第一個可用內存空間的指針,指針像遊標一樣向後移動,一段段內存就分配給了正在運行的程序的對象。在不需要太多垃圾回收的程序中,託管堆性能優於傳統的堆。
當第0代中沒有可以分配的有效內存時,就觸發了第0代中的一輪垃圾回收,它將刪除那些不再被引用的對象,並將當前正在使用的對象移至第1代。而當第0代垃圾回收後依然不能請求到充足的內存時,就啓動第1代垃圾回收。如果對各代都進行了垃圾回收後仍沒有可用的內存就會引發一個 OutOfMemoryException異常。
終結器(Finalize方法)
在有些情況下,類可以提供一個終結器在對象被銷燬時執行,終結器是一個名爲Finalize的受保護的方法:
protected void Finalize()
{
base.Finalize();
//釋放外部資源
}
垃圾回收器使用名爲“終止隊列”的內部結構跟蹤具有 Finalize 方法的對象。每次您的應用程序創建具有 Finalize 方法的對象時,垃圾回收器都在終止隊列中放置一個指向該對象的項。託管堆中所有需要在垃圾回收器回收其內存之前調用它們的終止代碼的對象都在終止隊列中含有項。(實現Finalize方法或析構函數對性能可能會有負面影響,因此應避免不必要地使用它們。用Finalize方法回收對象使用的內存需要至少兩次垃圾回收。當垃圾回收器執行回收時,它只回收沒有終結器的不可訪問對象的內存。這時,它不能回收具有終結器的不可訪問對象。它改爲將這些對象的項從終止隊列中移除並將它們放置在標爲準備終止的對象列表中。該列表中的項指向託管堆中準備被調用其終止代碼的對象。垃圾回收器爲此列表中的對象調用Finalize方法,然後將這些項從列表中移除。後來的垃圾回收將確定終止的對象確實是垃圾,因爲標爲準備終止對象的列表中的項不再指向它們。在後來的垃圾回收中,實際上回收了對象的內存。概括而言,就是將垃圾回收分爲了三個階段,第一個階段回收沒有Finalize方法或者析構函數的對象,第二個階段調用那些析構函數或者Finalize方法,第三個階段回收那些剛調用了析構函數或者Finalize方法的對象。)
終結器(finalizer)是在回收過程中,由垃圾回收器調用的方法。如何含有終結器的對象到了G2中,那麼就會需要非常長的時間來回收。事實上,根據應用程序運行時間的長短,對象很有機會直到應用程序退出之前都不會被回收(特別是其中包含的重要的資源得不得釋放,將會對性能產生很大的影響,比如說數據庫連接得不到釋放。)
Dispose方法
在不使用終結器時,可以考慮使用Dispose方法,你可以使用這個方法來釋放所保存包括的在託管對象引用在內的任何資源。系統類中如何實現了Dispose方法,那麼一般Dispose方法中都包含了SuppressFinalize方法,這個方法會告知系統這個類已經不再需要析構了,這樣可以提高釋放資源的效率。所以在自定義類中的Dispose方法應該調用GC.SuppressFinalize來告知運行時這些對象不需要析構。如下所示:
public void Dispose(){
object.Dispose();
dbConnection.Dispose();
GC.SuppressFinalize(this);//申明不需要終結
}
創建並使用了Dispose方法的對象,就需要使用完該對象之後調用這些方法,最好是在Finally中調用。
System.GC類
GC類包含了可使用戶與垃圾回收機制進行互操作的靜態方法,包括髮起新一輪垃圾回收操作的方法。確定某對象當前所在代的方法及當前分配內存空間的方法。
GC.Collect();//無參時將發起一輪全面的回收。(完全回收之前,應用程序會停止響應,因此不建議使用。)
GC.Collect(i);//(0<=i<=2)對第i代進行垃圾回收。
GetTotalMemory將返因分配於託管堆上的內存空間總量。當參數爲True時,在計算之前將進行一輪全面的垃圾回收。如下所示:
long totalMemory = System.GC.GetTotalMemory(True);
下面是 在.NET Framework 2.0 版中是新增的公共方法:
通知運行庫在安排垃圾回收時應考慮分配大量的非託管內存
public static void AddMemoryPressure (long bytesAllocated)//bytesAllocated已分配的非託管內存的增量。
返回已經對對象的指定代進行的垃圾回收次數。
public static int CollectionCount (int generation)
通知運行庫已釋放非託管內存,在安排垃圾回收時不需要再考慮它。
public static void RemoveMemoryPressure (long bytesAllocated)
C# 中的析構函數實際上是重寫了 System.Object 中的虛方法 Finalize
三種最常的方法如下:
1. 析構函數;(由GC調用,不確定什麼時候會調用)
2. 繼承IDisposable接口,實現Dispose方法;(可以手動調用。比如數據庫的連接,SqlConnection.Dispose(),因爲如果及時釋放會影響數據庫性能。這時候會用到這個,再如:文件的打開,如果不釋放會影響其它操作,如刪除操作。調用Dispose後這個對象就不能再用了,就等着被GC回收。)
3. 提供Close方法。(類似Dispose但是,當調用完Close方法後,可以通過Open重新打開)
析構函數不能顯示調用,而對於後兩種方法來說,都需要進行顯示調用才能被執行。而Close與Dispose這兩種方法的區別在於,調用完了對象的Close方法後,此對象有可能被重新進行使用;而Dispose方法來說,此對象所佔有的資源需要被標記爲無用了,也就是此對象要被銷燬,不能再被使用。
|
析構函數 |
Dispose方法 |
Close方法 |
意義 |
銷燬對象 |
銷燬對象 |
關閉對象資源 |
調用方式 |
不能被顯示調用,在GC回收是被調用 |
需要顯示調用 或者通過using語句 |
需要顯示調用 |
調用時機 |
不確定 |
確定,在顯示調用或者離開using程序塊 |
確定,在顯示調用時 |
下面提供一個模式來結合上面的 析構函數和Dispose方法。
<span style="font-size:14px;">public class BaseResource: IDisposable
{ //前面我們說了析構函數實際上是重寫了 System.Object 中的虛方法 Finalize, 默認情況下,一個類是沒有析構函數的,也就是說,對象被垃圾回收時不會被調用Finalize方法
~BaseResource()
{ // 爲了保持代碼的可讀性性和可維護性,千萬不要在這裏寫釋放非託管資源的代碼
// 必須以Dispose(false)方式調用,以false告訴Dispose(bool disposing)函數是從垃圾回收器在調用Finalize時調用的
Dispose(false);
}
// 無法被客戶直接調用
// 如果 disposing 是 true, 那麼這個方法是被客戶直接調用的,那麼託管的,和非託管的資源都可以釋放
// 如果 disposing 是 false, 那麼函數是從垃圾回收器在調用Finalize時調用的,此時不應當引用其他託管對象所以,只能釋放非託管資源
protected virtual void Dispose(bool disposing)
{
// 那麼這個方法是被客戶直接調用的,那麼託管的,和非託管的資源都可以釋放
if(disposing)
{
// 釋放 託管資源
OtherManagedObject.Dispose();
}
//釋放非託管資源
DoUnManagedObjectDispose();
// 那麼這個方法是被客戶直接調用的,告訴垃圾回收器從Finalization隊列中清除自己,從而阻止垃圾回收器調用Finalize方法.
if(disposing)
GC.SuppressFinalize(this);
}
//可以被客戶直接調用
public void Dispose()
{
//必須以Dispose(true)方式調用,以true告訴Dispose(bool disposing)函數是被客戶直接調用的
Dispose(true);
}
} </span>
上面的範例達到的目的:
1/ 如果客戶沒有調用Dispose(),未能及時釋放託管和非託管資源,那麼在垃圾回收時,還有機會執行Finalize(),釋放非託管資源,但是造成了非託管資源的未及時釋放的空閒浪費
2/ 如果客戶調用了Dispose(),就能及時釋放了託管和非託管資源,那麼該對象被垃圾回收時,不會執行Finalize(),提高了非託管資源的使用效率並提升了系統性能
最後:
如果類中使用了非託管資源,則要考慮提供Close方法,和Open方法。並在您的Dispose方法中先調用 Close方法。
在使用已經有類時,如SqlConnection。如果暫時不用這個連接,可以考慮用Close()方法。如果不用了就考慮調用Dispose()方法。