一、什麼是GC
GC如其名,就是垃圾收集,當然這裏僅就內存而言。Garbage Collector(垃圾收集器,在不至於混淆的情況下也成爲GC)以應用程序的root爲基礎,遍歷應用程序在Heap上動態分配的所有對象[2],通過識別它們是否被引用來確定哪些對象是已經死亡的、哪些仍需要被使用。已經不再被應用程序的root或者別的對象所引用的對象就是已經死亡的對象,即所謂的垃圾,需要被回收。這就是GC工作的原理。爲了實現這個原理,GC有多種算法。比較常見的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統.NET CLR,Java VM和Rotor都是採用的Mark Sweep算法。(此段內容來自網絡)
.NET的GC機制有這樣兩個問題:
首先,GC並不是能釋放所有的資源。它不能自動釋放非託管資源。
第二,GC並不是實時性的,這將會造成系統性能上的瓶頸和不確定性。
GC並不是實時性的,這會造成系統性能上的瓶頸和不確定性。所以有了IDisposable接口,IDisposable接口定義了Dispose方法,這個方法用來供程序員顯式調用以釋放非託管資源。使用using語句可以簡化資源管理。
二、託管資源和非託管資源
託管資源指的是.NET可以自動進行回收的資源,主要是指託管堆上分配的內存資源。託管資源的回收工作是不需要人工干預的,有.NET運行庫在合適調用垃圾回收器進行回收。
非託管資源指的是.NET不知道如何回收的資源,最常見的一類非託管資源是包裝操作系統資源的對象,例如文件,窗口,網絡連接,數據庫連接,畫刷,圖標等。這類資源,垃圾回收器在清理的時候會調用Object.Finalize()方法。默認情況下,方法是空的,對於非託管對象,需要在此方法中編寫回收非託管資源的代碼,以便垃圾回收器正確回收資源。
在.NET中,Object.Finalize()方法是無法重載的,編譯器是根據類的析構函數來自動生成Object.Finalize()方法的,所以對於包含非託管資源的類,可以將釋放非託管資源的代碼放在析構函數。
三、關於GC優化的一個例子
正常情況下,我們是不需要去管GC這些東西的,然而GC並不是實時性的,所以我們的資源使用完後,GC什麼時候回收也是不確定的,所以會帶來一些諸如內存泄漏、內存不足的情況,比如我們處理一個約500M的大文件,用完後GC不會立刻執行清理來釋放內存,因爲GC不知道我們是否還會使用,所以它就等待,先去處理其他的東西,過一段時間後,發現這些東西不再用了,才執行清理,釋放內存。
下面,來介紹一下GC中用到的幾個函數:
GC.SuppressFinalize(this); //請求公共語言運行時不要調用指定對象的終結器。
GC.GetTotalMemory(false); //檢索當前認爲要分配的字節數。 一個參數,指示此方法是否可以等待較短間隔再返回,以便系統回收垃圾和終結對象。
GC.Collect(); //強制對所有代進行即時垃圾回收。
GC運行機制
寫代碼前,我們先來說一下GC的運行機制。大家都知道GC是一個後臺線程,他會週期性的查找對象,然後調用Finalize()方法去消耗他,我們繼承IDispose接口,調用Dispose方法,銷燬了對象,而GC並不知道。GC依然會調用Finalize()方法,而在.NET 中Object.Finalize()方法是無法重載的,所以我們可以使用析構函數來阻止重複的釋放。我們調用完Dispose方法後,還有調用GC.SuppressFinalize(this) 方法來告訴GC,不需要在調用這些對象的Finalize()方法了。
下面,我們新建一個控制檯程序,加一個Factory類,讓他繼承自IDispose接口,代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace GarbageCollect { public class Factory : IDisposable { private StringBuilder sb = new StringBuilder(); List list = new List(); //拼接字符串,創造一些內存垃圾 public void MakeSomeGarbage() { for (int i = 0; i < 50000; i++) { sb.Append(i.ToString()); } } //銷燬類時,會調用析構函數 ~Factory() { Dispose(false); } public void Dispose() { Dispose(true); } protected virtual void Dispose(bool disposing) { if (!disposing) { return; } sb = null; GC.Collect(); GC.SuppressFinalize(this); } } }
只有繼承自IDispose接口,使用這個類時才能使用Using語句,在main方法中寫如下代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; namespace GarbageCollect { class Program { static void Main(string[] args) { using(Factory f = new Factory()) { f.MakeSomeGarbage(); Console.WriteLine("Total memory is {0} KBs.", GC.GetTotalMemory(false) / 1024); } Console.WriteLine("After GC total memory is {0} KBs.", GC.GetTotalMemory(false) / 1024); Console.Read(); } } }
運行結果如下,可以看到資源運行MakeSomeGarbage()函數後的內存佔用爲1796KB,釋放後成了83Kb.
代碼運行機制:
我們寫了Dispose方法,還寫了析構函數,那麼他們分別什麼時候被調用呢?我們分別在兩個方法上面下斷點。調試運行,你會發現先走到了Dispose方法上面,知道程序運行完也沒走析構函數,那是因爲我們調用了GC.SuppressFinalize(this)方法,如果去掉這個方法後,你會發現先走Dispose方法,後面又走析構函數。所以,我們可以得知,如果我們調用Dispose方法,GC就會調用析構函數去銷燬對象,從而釋放資源。
四、什麼時候該調用GC.Collect
這裏爲了讓大家看到效果,我顯示調用的GC.Collect()方法,讓GC立刻釋放內存,但是頻繁的調用GC.Collect()方法會降低程序的性能,除非我們程序中某些操作佔用了大量內存需要馬上釋放,纔可以顯示調用。下面是官方文檔中的說明:
垃圾回收 GC 類提供 GC.Collect 方法,您可以使用該方法讓應用程序在一定程度上直接控制垃圾回收器。通常情況下,您應該避免調用任何回收方法,讓垃圾回收器獨立運行。在大多數情況下,垃圾回收器在確定執行回收的最佳時機方面更有優勢。但是,在某些不常發生的情況下,強制回收可以提高應用程序的性能。當應用程序代碼中某個確定的點上使用的內存量大量減少時,在這種情況下使用 GC.Collect 方法可能比較合適。例如,應用程序可能使用引用大量非託管資源的文檔。當您的應用程序關閉該文檔時,您完全知道已經不再需要文檔曾使用的資源了。出於性能的原因,一次全部釋放這些資源很有意義。有關更多信息,請參見 GC.Collect 方法。
在垃圾回收器執行回收之前,它會掛起當前正在執行的所有線程。如果不必要地多次調用 GC.Collect,這可能會造成性能問題。您還應該注意不要將調用GC.Collect 的代碼放置在程序中用戶可以經常調用的點上。這可能會削弱垃圾回收器中優化引擎的作用,而垃圾回收器可以確定運行垃圾回收的最佳時間。
理解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; // 標識此對象已釋放
}
}