C#垃圾回收学习总结
生活随笔
收集整理的這篇文章主要介紹了
C#垃圾回收学习总结
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
淺談C#垃圾回收
http://www.cnblogs.com/cuiyiming/archive/2013/03/26/2981931.html理解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; // 標識此對象已釋放
? ? ? }
? }
參考了MSDN和網上的一些資料,第一次寫博文請各位大俠多多指點!
========
c# -- 對象銷毀和垃圾回收
http://www.cnblogs.com/yang_sy/p/3784151.html有些對象需要顯示地銷毀代碼來釋放資源,比如打開的文件資源,鎖,操作系統句柄和非托管對象。在.NET中,這就是所謂的對象銷毀,它通過IDisposal接口來實現。不再使用的對象所占用的內存管理,必須在某個時候回收;這個被稱為無用單元收集的功能由CLR執行
。
對象銷毀和垃圾回收的區別在于:對象銷毀通常是明確的策動;而垃圾回收完全是自動地。換句話說,程序員負責釋放文件句柄,鎖,以及操作系統資源;而CLR負責釋放內存。
本章將討論對象銷毀和垃圾回收,還描述了C#處理銷毀的一個備選方案--Finalizer及其模式。最后,我們討論垃圾回收器和其他內存管理選項的復雜性。
對象銷毀 垃圾回收
1)IDisposal接口
2) Finalizer 垃圾回收
對象銷毀用于釋放非托管資源 垃圾回收用于自動釋放不再被引用的對象所占用的內存;并且垃圾回收什么時候執行時不可預計的
為了彌補垃圾回收執行時間的不確定性,可以在對象銷毀時釋放托管對象占用的內存 ?
?
IDisposal,Dispose和Close
image
.NET Framework定義了一個特定的接口,類型可以使用該接口實現對象的銷毀。該接口的定義如下:
public interface IDisposable
{
void Dispose();
}
C#提供了鴘語法,可以便捷的調用實現了IDisposable的對象的Dispose方法。比如:
using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open))
{
// ... Write to the file ...
}
編譯后的代碼與下面的代碼是一樣的:
復制代碼
FileStream fs = new FileStream ("myFile.txt", FileMode.Open);
try
{
// ... Write to the file ...
}
finally
{
if (fs != null) ((IDisposable)fs).Dispose();
}
復制代碼
finally語句確保了Dispose方法的調用,及時發生了異常,或者代碼在try語句中提前返回。
在簡單的場景中,創建自定義的可銷毀的類型值需要實現IDisposable接口即可
sealed class Demo : IDisposable
{
public void Dispose()
{
// Perform cleanup / tear-down.
...
}
}
請注意,對于sealed類,上述模式非常適合。在本章后面,我們會介紹另外一種銷毀對象的模式。對于非sealed類,我們強烈建議時候后面的那種銷毀對象模式,否則在非sealed類的子類中,也希望實現銷毀時,會發生非常詭異的問題。
對象銷毀的標準語法
Framework在銷毀對象的邏輯方面遵循一套規則,這些規則并不限用于.NET Framework或C#語言;這些規則的目的是定義一套便于使用的協議。這些協議如下:
一旦銷毀,對象不可恢復。對象不能被再次激活,調用對象的方法或者屬性拋出ObjectDisposedException異常
重復地調用對象的Disposal方法會導致錯誤
如果一個可銷毀對象x包含,或包裝,或處理另外一個可銷毀對象y,那么x的Dispose方法自動調用x的Dispose方法,除非另有指令(不銷毀y)
這些規則同樣也適用于我們平常創建自定義類型,盡管它并不是強制性的。沒有誰能阻止你編寫一個不可銷毀的方法;然而,這么做,你的同事也許會用高射炮攻擊你。
對于第三條規則,一個容器對象自動銷毀其子對象。最好的一個例子就是,windows容器對象比如Form對著Panel。一個容器對象可能包含多個子控件,那你也不需要顯示地銷毀每個字對象:關閉或銷毀父容器會自動關閉其子對象。另外一個例子就是如果你在
DeflateStream包裝了FileStream,那么銷毀DeflateStream時,FileStream也會被銷毀--除非你在構造器中指定了其他的指令。
Close和Stop
有一些類型除了Dispose方法之外,還定義了Close方法。Framework對于Close方法并沒有保持完全一致性,但在幾乎所有情況下,它可以:
要么在功能上與Dispose一致
或只是Dispose的一部分功能
對于后者一個典型的例子就是IDbConnecton類型,一個Closed的連接可以再次被打開;而一個Disposed的連接對象則不能。另外一個例子就是Windows程序使用ShowDialog的激活某個窗口對象:Close方法隱藏該窗口;而Dispose釋放窗口所使用的資源。
有一些類定義Stop方法(比如Timer或HttpListener)。與Dipose方法一樣,Stop方法可能會釋放非托管資源;但是與Dispose方法不同的是,它允許重新啟動。
何時銷毀對象
銷毀對象應該遵循的規則是“如有疑問,就銷毀”。一個可以被銷毀的對象--如果它可以說話--那么將會說這些內容:
“如果你結束對我的使用,那么請讓我知道。如果只是簡單地拋棄我,我可能會影響其他實例對象、應用程序域、計算機、網絡、或者數據庫”
如果對象包裝了非托管資源句柄,那么經常會要求銷毀,以釋放句柄。例子包括Windows Form控件、文件流或網絡流、網絡sockets,GDI+畫筆、GDI+刷子,和bitmaps。與之相反,如果一個類型是可銷毀的,那么它會經常(但不總是)直接或間接地引用非托管句柄。這
是由于非托管句柄對操作系統資源,網絡連接,以及數據庫鎖之外的世界提供了一個網關(出入口),這就意味著使用這些對象時,如果不正確的銷毀,那么會對外面的世界代碼麻煩。
但是,遇到下面三種情形時,不要銷毀對象
通過靜態成員或屬性獲取一個共享的對象
如果一個對象的Dispose方法與你的期望不一樣
從設計的角度看,如果一個對象的Dispose方法不必要,且銷毀對象給程序添加了復雜度
第一種情況很少見。多數情形都可以在System.Drawing命名空間下找到:通過靜態成員或屬性獲取的GDI+對象(比如Brushed.Blue)就不能銷毀,這是因為該實現在程序的整個生命周期中都會用到。而通過構造器得到的對象實例,比如new SolidBrush,就應該銷毀,這
同樣適用于通過靜態方法獲取的實例對象(比如Font.FromHdc)。
第二種情況就比較常見。下表以System.IO和System.Data命名空間下類型舉例說明
類型 銷毀功能 何時銷毀
MemoryStream 防止對I/O繼續操作 當你需要再次讀讀或寫流
StreamReader,
StreamWriter 清空reader/writer,并關閉底層的流 當你希望底層流保持打開時(一旦完成,你必須改為調用StreamWriter的Flush方法)
IDbConnection 釋放數據庫連接,并清空連接字符串 如果你需要重新打開數據庫連接,你需要調用Close方法而不是Dispose方法
DataContext
(LINQ to SQL) 防止繼續使用 當你需要延遲評估連接到Context的查詢
第三者情況包含了System.ComponentModel命名空間下的這幾個類:WebClient, StringReader, StringWriter和BackgroundWorker。這些類型有一個共同點,它們之所以是可銷毀的是源于它們的基類,而不是真正的需要進行必要的清理。如果你需要在一個方法中使用這
樣的類型,那么在using語句中實例化它們就可以了。但是,如果實例對象需要持續一段較長的時間,并記錄何時不再使用它們以銷毀它們,就會給程序帶來不惜要的復雜度。在這樣的情況下,那么你就應該忽略銷毀對象。
選擇性地銷毀對象
正因為IDisposable實現類可以使用using語句來實例化,因而這可能很容易導致該實現類的Dispose方法延伸至不必要的行為。比如:
public sealed class HouseManager : IDisposable
{
public void Dispose()
{
CheckTheMail();
}
...
}
想法是該類的使用者可以選擇避免不必要的清理--簡單地說就是不調用Dispose方法。但是,這就需要調用者知道HouseManager類Dispose方法的實現細節。及時是后續添加了必要的清理行為也破壞了規則。
public void Dispose()
{
CheckTheMail(); // Nonessential
LockTheHouse(); // Essential
}
在這種情況下,就應該使用選擇性銷毀模式
public sealed class HouseManager : IDisposable { public readonly bool CheckMailOnDispose; public Demo (bool checkMailOnDispose) { CheckMailOnDispose = checkMailOnDispose; } public void Dispose() { if (CheckMailOnDispose) CheckTheMail(); LockTheHouse(); } ... }
這樣,任何情況下,調用者都可以調用Dispose--上述實現不僅簡單,而且避免了特定的文檔或通過反射查看Dispose的細節。這種模式在.net中也有實現。System.IO.Compression空間下的DeflateStream類中,它的構造器如下
public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen)
非必要的行為就是在銷毀對象時關閉內在的流(第一個參數)。有時候,你希望內部流保持打開的同時并銷毀DeflateStream以執行必要的銷毀行為(清空bufferred數據)
這種模式看起來簡單,然后直到Framework 4.5,它才從StreamReader和StreamWriter中脫離出來。結果卻是丑陋的:StreamWriter必須暴露另外一個方法(Flush)以執行必要的清理,而不是調用Dispose方法(Framework 4.5在這兩個類上公開一個構造器,以允許你保持
流處于打開狀態)。System.Security.Cryptography命名空間下的CryptoStream類,也遭遇了同樣的問題,當需要保持內部流處于打開時你要調用FlushFinalBlock銷毀對象。
銷毀對象時清除字段
在一般情況下,你不要在對象的Dispose方法中清除該對象的字段。然而,銷毀對象時,應該取消該對象在生命周期內所有訂閱的事件。退訂這些事件避免了接收到非期望的通知--同時也避免了垃圾回收器繼續對該對象保持監視。
設置一個字段用以指明對象是否銷毀,以便在使用者在該對象銷毀后訪問該對象拋出一個ObjectDisposedException,這是非常值得做的。一個好的模式就是使用一個public的制度的屬性:
public bool IsDisposed { get; private set; }
盡管技術上沒有必要,但是在Dispose方法清除一個對象所擁有的事件句柄(把句柄設置為null)也是非常好的一種實踐。這消除了在銷毀對象期間這些事件被觸發的可能性。
偶爾,一個對象擁有高度秘密,比如加密密鑰。在這種情況下,那么在銷毀對象時清除這樣的字段就非常有意義(避免被非授權組件或惡意軟件發現)。System.Security.Cryptography命令空間下的SymmetricAlgorithm類就屬于這種情況,因此在銷毀該對象時,調用
Array.Clear方法以清除加密密鑰。
自動垃圾回收機制
無論一個對象是否需要Dispose方法以實現銷毀對象的邏輯,在某個時刻,該對象在堆上所占用的內存空間必須釋放。這一切都是由CLR通過GC自動處理. 你不需要自己釋放托管內存。我們首先來看下面的代碼
public void Test()
{
byte[] myArray = new byte[1000];
}
當Test方法執行時,在內存的堆上分配1000字節的一個數組;該數組被變量myArray引用,這個變量存儲在變量棧上。當方法退出后,局部變量myArray就失去了存在的范疇,這也意味著沒有引用指向內存堆上的數組。那么該孤立的數組,就非常適合通過垃圾回收機制進
行回收。
垃圾回收機制并不會在一個對象變成孤立的對象之后就立即執行。與大街上的垃圾收集不一樣,.net垃圾回收是定期執行,盡享不是按照一個估計的計劃。CLR決定何時進行垃圾回收,它取決于許多因素,比如,剩余內存,已經分配的內存,上一次垃圾回收的時間。這就
意味著,在一個對象被孤立后到期占用的內存被釋放之間,有一個不確定的時間延遲。該延遲的范圍可以從幾納秒到數天。
垃圾回收和內存占用
垃圾收集試圖在執行垃圾回收的時間與程序的內存占用之間建立一個平衡。因此,程序可以占用比它們實際需要更多的內存,尤其特現在程序創建的大的臨時數組。
你可以通過Windows任務管理器監視某一個進程內存的占用,或者通過編程的方式查詢性能計數器來監視內存占用:
// These types are in System.Diagnostics:
string procName = Process.GetCurrentProcess().ProcessName;
using (PerformanceCounter pc = new PerformanceCounter
("Process", "Private Bytes", procName))
Console.WriteLine (pc.NextValue());
上面的代碼查詢內部工作組,返回你當前程序的內存占用。尤其是,該結果包含了CLR內部釋放,以及把這些資源讓給操作系統以供其他的進程使用。
根
根就是指保持對象依然處于活著的事物。如果一個對象不再直接或間接地被一個根引用,那么該對象就適合于垃圾回收。
一個跟可以是:
一個正在執行的方法的局部變量或參數(或者調用棧中任意方法的局部變量或參數)
一個靜態變量
存貯在結束隊列中的一個對象
正在執行的代碼可能涉及到一個已經刪除的對象,因此,如果一個實例方法正在執行,那么該實例方法的對象必然按照上述方式被引用。
請注意,一組相互引用的對象的循環被視作無根的引用。換一種方式,也就是說,對象不能通過下面的箭頭指向(引用)而從根獲取,這也就是引用無效,因此這些對象也將被垃圾回收器處理。
image
Finalizers
在一個對象從內存釋放之前,如果對象包含finalizer,那么finalizer開始運行。一個finalizer的聲明類似構造器函數,但是它使用~前綴符號
class Test
{
? ? ~Test()
? ? {
? ? ? ? // finalizer logic ...
? ? }
}
(盡管與構造器的聲明相似,finalizer不能被聲明為public或static,也不能有參數,還不能調用其基類)
Finalizer是可能的,因為垃圾收集工作在不同的時間段。首先,垃圾回收識別沒有使用的對象以刪除該對象。這些待刪除的對象如果沒有Finalizer那么就立即刪除。而那些擁有finalizer的對象會被保持存活并存在放到一個特殊的隊列中。
在這一點上,當你的程序在繼續執行的時候,垃圾收集也是完整的。而Finalizer線程卻在你程序運行時,自動啟動并在另外一個線程中并發執行,收集擁有Finalizer的對象到特殊隊列,然后執行它們的終止方法。在每個對象的finalizer方法執行之前,它依然非?;钴S
--排序行為視作一個跟對象。而一檔這些對象被移除隊列,并且這些對象的fainalizer方法已經執行,那么這些對象就變成孤立的對象,會在下一階段的垃圾回收過程中被回收。
Finalizer非常有用,但它們也有一些限制:
Finalizer減緩內存分配和收集(因為GC需要追蹤那些Finalizer在運行)
Finalizer延長對象及其所引用對象的生命周期(這些對象只有在下一次垃圾回收運行過程中被真正地刪除)
對于一組對象,Finalizer的調用順序是不可預測的
你不能控制一個對象的finalizer何時被調用
如果一個對象的finalizer被阻塞,那么其他對象不能處置(Finalized)
如果程序沒有卸載(unload)干凈,那么finalizer會被忽略
總之,finalizer在一定程度上就好比律師--一旦有訴訟那么你確實需要他們,一般你不想使用他們,除非萬不得已。如果你使用他們,那么你需要100%確保你了解他們會為你做什么。
下面是實施finalizer的一些準則:
確保finalizer快速執行
絕對不要在finalier中使用阻塞
不要引用其他可finalizable對象
不要拋出異常
?
在Finalizer中調用Dispose
一個流行的模式是使finalizer調用Dispose方法。這么做是有意義的,尤其是當清理工作不是緊急的,并且通過調用Dispose加速清理;那么這樣的方式更多是一個優化,而不是一個必須。
下面的代碼展示了該模式是如何實現的
class Test : IDisposable
{
public void Dispose() // NOT virtual
{
Dispose (true);
GC.SuppressFinalize (this); // Prevent finalizer from running.
}
protected virtual void Dispose (bool disposing)
{
if (disposing)
{
// Call Dispose() on other objects owned by this instance.
// You can reference other finalizable objects here.
// ...
}
// Release unmanaged resources owned by (just) this object.
// ...
}
?Test()
{
Dispose (false);
}
}
Dispose方法被重載,并且接收一個bool類型參數。而沒有參數的Dispose方法并沒有被聲明為virtual,只是在該方法內部調用了帶參數的Dispose方法,且傳遞的參數的值為true。
帶參數的Dispose方法包含了真正的處置對象的邏輯,并且它被聲明為protected和virtual。這樣就可以保證其子類可以添加自己的處置邏輯。參數disposing標記意味著它在Dispose方法中被正確的調用,而不是從finalizer的最后采取模式所調用。這也就表明,如果調
用Dispose時,其參數disposing的值如果為false,那么該方法,在一般情況下,都會通過finalizer引用其他對象(因為,這樣的對象可能自己已經被finalized,因此處于不可預料的狀態)。這里面涉及的規則非常多!當disposing參數是false時,在最后采取的模式中
,仍然會執行兩個任務:
釋放對操作系統資源的直接引用(這些引用可能是因為通過P/Invoke調用Win32 API而獲取到)
刪除由構造器創建的臨時文件
為了使這個模式更強大,那么任何會拋出異常的代碼都應包含在一個try/catch代碼塊中;而且任何異常,在理想狀態下,都應該被記錄。此外,這些記錄應當今可能既簡單又強大。
請注意,在無參數的Dispose方法中,我們調用了GC.SuppressFinalize方法,這會使得GC在運行時,阻止finalizer執行。從技術角度講,這沒有必要,因為Dispose方法必然會被重復調用。但是,這么做會改進性能,因為它允許對象(以及它所引用的對象)在單個循環
中被垃圾回收器回收。
復活
假設一個finalizer修改了一個活的對象,使其引用了一個“垂死”對象。那么當下一次垃圾回收發生時,CLR會查看之前垂死的對象是否確實沒有任何引用指向它--從而確定是否對其執行垃圾回收。這是一個高級的場景,該場景被稱作復活(resurrection)。
為了證實這點,假設我們希望創建一個類管理一個臨時文件。當類的實例被回收后,我們希望finalizer刪除臨時文件。這看起來很簡單
復制代碼
public class TempFileRef
{
public readonly string FilePath;
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef() { File.Delete (FilePath); }
}
實際,上訴代碼存在bug,File.Delete可能會拋出一個異常(引用缺少權限,或者文件處于使用中) 。這樣的異常會導致拖垮整個程序(還會阻止其他finalizer執行)。我們可以通過一個空的catch代碼塊來“消化”這個異常,但是這樣我們就不能獲取任何可能發生的錯誤
。 調用其他的錯誤報告API也不是我們所期望的,因為這么做會加重finalizer線程的負擔,并且會妨礙對其他對象進行垃圾回收。 我們期望顯示finalization行為簡單、可靠、并快速。
一個好的解決方法是在一個靜態集合中記錄錯誤信息:
public class TempFileRef
{
static ConcurrentQueue<TempFileRef> _failedDeletions
= new ConcurrentQueue<TempFileRef>();
public readonly string FilePath;
public Exception DeletionError { get; private set; }
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch (Exception ex)
{
DeletionError = ex;
_failedDeletions.Enqueue (this); // Resurrection
}
}
}
把對象插入到靜態隊列_failedDeletions中,使得該對象處于引用狀態,這就確保了它仍然保持活著的狀態,直到該對象最終從隊列中出列。
GC.ReRegisterForFinalize
一個復活對象的finalizer不會再次運行--除非你調用GC.ReRegisterForFinalize
在下面的例子中,我們試圖在一個finalizer中刪除一個臨時文件。但是如果刪除失敗,我們就重新注冊帶對象,以使其在下一次垃圾回收執行過程中被回收。
public class TempFileRef
{
public readonly string FilePath;
int _deleteAttempt;
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch
{
if (_deleteAttempt++ < 3) GC.ReRegisterForFinalize (this);
}
}
}
如果第三次嘗試失敗后,finalizer會靜悄悄地放棄刪除臨時文件。我們可以結合上一個例子增強該行為--換句話說---那就是在第三次失敗后,把該對象加入到_failedDeletions隊列中。
垃圾回收工作原理
標準的CLR使用標記和緊湊的GC對存儲托管堆上的對象執行自動內存管理。GC可被視作一個可被追蹤的垃圾回收器,在這個回收器中,它(GC)不與任何對象接觸;而是被間歇性地被喚醒,然后跟蹤存儲在托管堆對象圖,以確定哪些對象可以被視為垃圾,進而對這些對象
執行垃圾回收。
當(通過new關鍵字)執行內存分配是,或當已經分配的內存達到了某一閥值,亦或當應用程序占用的內存減少時,GC啟動一個垃圾收集。這個過程也可以通過手動調用System.GC.Collect方法啟動。在一個垃圾回收過程中,所有線程都可能被凍結。
GC從根對象引用開始,查找貴根對象對應的整個對象圖,然后把所有的對象標記為可訪問的對象。一旦這個過程完成,所有被標記為不再使用的對象,將被垃圾回收器回收。
沒有finalizer的不再使用的對象立即被處置;而擁有finalizer的不再使用對象將會在GC完成之后,在finalizer線程上排隊以等待處理。這些對象(在finalizer線程上排隊的對象)會在下一次垃圾回收過程中被回收(除非它們又復活了)。
而那些剩余的“活”對象(還需要使用的對象),被移動到堆疊開始位置(壓縮),這樣以騰出更多空間容納更多對象。改壓縮過程有兩個目的:其一是避免了內存碎片,這樣就使得在為新對象分配空間后,GC只需使用簡單的策略即可,因為新的對象總是分配在堆的尾
部。其二就是避免了維護一個非常耗時的內存片段列表任務。
在執行完一次垃圾回收之后,為新對象分配內存空間時,如果沒有足夠的空間可以使用,操作系統不能確保更多的內存使用時,拋出OutOfMemoryException。
優化技術
GC引入了各種優化技術來減少垃圾回收的時間。
通用垃圾回收
最重要的優化就是垃圾回收時通用的。其優點是:盡管快速分配和處置大量對象,某些對象是長存內存,因此他們不需要被垃圾回收追蹤。
基本上,GC把托管堆分為三類:Gen0是在堆上剛剛分配的對象;Gen1經過一次垃圾回收后仍然存活的對象;剩余的為Gen2。
CLR限制Gen0的大小(在32位CLR中,最大16MB,一般大小為數百KB到幾MB)。當Gen0空間耗盡,GC便觸發一個Gen0垃圾回收--該垃圾回收發生非常頻繁。對于Gen1,GC也應用了一個相似的大小限制,因為Gen1垃圾回收也是相當頻繁并且快速完成。Gen2包含了所有類型的
垃圾回收,然而,發生在Gen2的垃圾回收執行時間長,并且也不會經常發生。下圖展示了一個完全垃圾回收:
image
如果真要列出一組大概的數字,那么Gen0垃圾回收執行耗費少于1毫秒,在一個應用程序中一般不會被注意到。而全垃圾回收,如果程序包含大的圖形對象,則可能會耗費100毫秒。執行時間受諸多因素影響二次可能會有不同,尤其是Gen2的垃圾回收,它的尺寸是沒有限
定的。
段時間存活的對象,如果使用GC會非常有效。比如下面示例代碼中的StringBuilder,就會很快地被發生在Gen0上的垃圾回收所回收。
string Foo()
{
var sb1 = new StringBuilder ("test");
sb1.Append ("...");
var sb2 = new StringBuilder ("test");
sb2.Append (sb1.ToString());
return sb2.ToString();
}
大對象堆
GC為大對象(大小超過85,000字節)使用單獨的堆。這就避免了大量消耗Gen0堆。因為在Gen0上沒有大對象,那么就不會出現分配一組16MB的對象(這些對象由大對象組成)之后,馬上觸發垃圾回收。
大對象堆不適合于壓縮,這是因為發生垃圾回收時,移動內存大塊的代價非常高。如果這么做,會帶來下面兩個后果:
內存分配低效,這是因為GC不能總是把對象分配在堆的尾部,它還必須查看中間的空隙,那么這就要求維護一個空白內存塊鏈表。
大對象堆適合于片段化。這意味著凍結一個對象,會在大對象堆上生成一個空洞,這個空洞很難在再被填充。比如,一個空洞留下了86000字節的空間,那么這個空間就只能被一個85000字節或86000自己的對象填充(除非與另外的一個空洞連接在一起,形成更大的空間)
大對象堆還是非通用的堆,大對象堆上的所有對象被視作Gen2
并發回收和后臺回收
GC在執行垃圾回收時,必須釋放(阻塞)你的程序所使用的線程。在這個期間包含了Gen0發生的時間和Gen1發生的時間。
由于執行Gen2回收可能占用較長的時間,因此GC會在你的程序運行時,堆Gen2回收進行特殊的嘗試。該優化技術僅應用于工作站的CLR平臺,一般應用于windows桌面系統(以及所有運行獨立程序的Windows)。原因是由于阻塞線程進行垃圾回收所帶來的延遲對于沒有用戶
接口的服務器應用程序一般不會帶來問題。
這種對于工作站的優化歷史上稱之為并發回收。從CLR4.0kaishi ,它發生了革新并重命名為后臺回收。后臺回收移除了一個限制,由此,并發回收不再是并發的,如果Gen0部分已經執行完而Gen2回收還正在執行。這就意味著,從CLR4.0開始,持續分配內存的應用程序會
更加敏感。
GC通知(適用于服務端CLR)
從Framework 3.5 SP1開始,服務器版本的CLR在一個全GC將要發生時,向你發送通知。你可以在服務器池配置中配置該特性:在一個垃圾回收執行之前,把請求轉向到另外一臺服務器。然后你立即調查垃圾回收,并等待其完成,在垃圾回收執行完成之后,把請求轉回到
當前服務器。
通過調用GC.RegisterForFullGCNotification,可以啟用GC通知。然后,啟動另外一個線程,該線程首先調用GC.WaitForFullGCApproach,當該方法返回GCNotificationStatus指明垃圾回收已經進入等待執行的隊列,那么你就可以把請求轉向到其他的服務器,然后手執
行一次手動垃圾回收(見下節)。然后,你調用GC.WaitForFullGCComplete方法,當該方法返回時,GC完成;那么該服務器就可以開始再次接收請求。然后在有需要的時候,你可以再次執行上述整個過程。
強制垃圾回收
通過調用GC.Collect方法,你可以隨時手動強制執行一次垃圾回收。調用GC.Collect沒有提供任何參數會執行一次完全垃圾回收。如果你提供一個整數類型的參數,那么執行對應的垃圾回收。比如GC.Collect(0)執行Gen0垃圾回收。
// Forces a collection of all generations from 0 through Generation.
//
public static void Collect(int generation) {
? ? Collect(generation, GCCollectionMode.Default)
}
// Garbage Collect all generations.
//
[System.Security.SecuritySafeCritical] ?// auto-generated
public static void Collect() {
? ? //-1 says to GC all generations.
? ? _Collect(-1, (int)InternalGCCollectionMode.Blocking);
}
一般地,允許GC去決定何時執行垃圾回收可以得到最好的性能;這是因為強制垃圾回收會把Gen0的對象不必要地推送到Gen1(Gen1不必要地推送到Gen2),從而影響性能。這還會擾亂GC自身的調優能力--在程序運行時,GC動態地調整每種垃圾回收的臨界值以最大限度地
提高性能。
但是,也有另外。最常見的可以執行手動垃圾回收的場景就是當一個應用程序進入休眠狀態,比如執行日常工作的windows服務。這樣的程序可能使用了System.Timters.Timer以每隔24小時觸發一次行為。當該行為完成之后,在接著的24小時之內沒有任何代碼會執行,那
就意味著,在這段時間內,不會分配任何內存,因此GC就沒有機會被激活。服務在執行時所消耗的任何內存,在接著的24小時都會被持續占用--甚至是空對象圖。那么解決方法就是在日常的行為完成之后調用GC.Collect()方法進行垃圾回收。
為了回收由于finalizer延遲回收的對象,你可以添加一行額外的代碼以調用WaitForPendingFinalizers,然后再調用一次垃圾回收
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
另外一種調用GC.Collect方法的場景是當你測試一個有Finazlier的類時。
?
內存壓力
.NET運行時基于一些列因素決定何時啟動垃圾回收,其中一個因素就是機器內存的總負載。 如果程序使用了非托管內存,那么運行時會對其內存的使用情況持盲目地樂觀的態度,這是因為CLR之關心托管內存。通過告訴CLR已經分配了特定量的非托管內存內存,來減輕
CLR的盲目性;調用CG.AddMemoryPresure方法可以完成該目的。如果取消該行為(當所占用的托管內存已經被釋放),那么可以調用GC.RemoveMemoryPressure。
管理內存泄漏
在非托管語言中,比如C++,你必須記住當對象不再使用時,應手動地釋放內存;否則,將導致內存泄漏。在托管世界中,內存泄漏這種錯誤時不可能發生的,這歸功于CLR的自動垃圾回收。
盡管如此,大型的和復雜的.NET程序也會出現內存泄漏;只不錯內存泄漏的方式比較溫和,但具有相同的癥狀和結果:在程序的生命周期內,它消耗越來越多的內存,到最后導致程序重啟。好消息是,托管內存泄漏通常容易診斷和預防。
托管內存泄漏是由不再使用的活對象引起,這些對象之所以存活是憑借不再使用引用或者被遺忘的引用。一種常見的例子就是事件處理器--它們堆目標對象保存了一個引用(除非目標是靜態方法)。比如,下面的類:
復制代碼
class Host
{
public event EventHandler Click;
}
class Client
{
Host _host;
public Client (Host host)
{
_host = host;
_host.Click += HostClicked;
}
void HostClicked (object sender, EventArgs e) { ... }
}
復制代碼
下面的測試類包含1個方法實例化了1000個Client對象
復制代碼
class Test
{
static Host _host = new Host();
public static void CreateClients()
{
Client[] clients = Enumerable.Range (0, 1000)
.Select (i => new Client (_host))
.ToArray();
// Do something with clients ...
}
}
復制代碼
你可能會認為,當CeateClients方法結束后,這個1000個Client對象理解適用于垃圾回收。很不幸,每個Client對象都包含一個引用:_host對象,并且該對象的Click事件引用每個Client實例。 如果Click事件不觸發,那么就不會引起注意,或者HostClicked方法不做任
何事情也不會引起注意。
解決這個問題的一種方式就是使Client類實現接口IDisposable,并且在dispose方法中,移除時間處理器
public void Dispose() { _host.Click -= HostClicked; }
Client實例的使用者,在使用完實例之后,調用Client類的dispose方法處置該實例
Array.ForEach (clients, c => c.Dispose());
下面的對比展示兩種方式的差別
CLR Profiler
Index 實現IDisposable 未實現IDisposable
Time line image image
Heap statistics image image
GC Generatation Sizes image image
?
計時器
不要忘記timmers也會引起內存泄漏。根據計時器的種類,會引發兩種不同的內存泄漏。首先我們來看System.Timers命名空間下的計時器。在下面的例子中,Foo類每秒調用一次tmr_Elapsed方法
復制代碼
using System.Timers;
class Foo
{
Timer _timer;
Foo()
{
_timer = new System.Timers.Timer { Interval = 1000 };
_timer.Elapsed += tmr_Elapsed;
_timer.Start();
}
void tmr_Elapsed (object sender, ElapsedEventArgs e) { ... }
}
復制代碼
很不幸,Foo的實例決定不會被回收。原因在于.NET Framework本身持有對計活動的時器的引用,從而導致.net framework會觸發這些計時器的Elapsed事件。因此
.NET Framework將使_timer處于活動狀態
通過tmr_Elapsed事件處理器,_timer將使Foo實現處于活動狀態
當你意識到Timer實現了IDisposable接口之后,解決的方法就在也明顯不過了。處置Timer實例以停止計時器,并確保.NET Framework不再引用該計時器對象。
class Foo : IDisposable
{
...
public void Dispose() { _timer.Dispose(); }
}
相對于我們上面討論的內容,WPF和Windows窗體的計時器表現出完全相同的方式。
然而,System.Threading命名空間下的計時器確是一個特例。.NET Framework沒有引用活動線程計時器;想法,卻直接引用回調代理。這就意味著如果你忘記處置線程計時器,那么finalizer會自動觸發并停止計時器然后處置該計時器。比如:
復制代碼
static void Main()
{
var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000);
GC.Collect();
System.Threading.Thread.Sleep (10000); // Wait 10 seconds
}
static void TimerTick (object notUsed) { Console.WriteLine ("tick"); }
復制代碼
如果上面的代碼編譯為發布模式,那么計時器會被回收,并且在它再次觸發之前被處置(finalized)。同樣地,我們可以在計時器結束后通過處置該計數器以修復這個問題
using (var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000))
{
GC.Collect();
System.Threading.Thread.Sleep (10000); // Wait 10 seconds
}
using語句會隱式地調用tmr.Dispose方法,以確保tmr變量確實處于“使用(活動狀態)”;因此不會在代碼塊結束之前被當作是死對象。諷刺的是,調用Dispose方法實際上使對象存活的時間更長了。
診斷內存泄漏
避免托管內存泄漏的最簡單方式就是在編寫應用程序時就添加監控內存占用。你可以在程序中通過調用下面的代碼來獲取當前內存的使用情況
long memoryUsed = GC.GetTotalMemory (true);
如果你采取測試驅動開發,那么你可以使用單元測試判斷是否按照期望釋放了內存。入股這樣的判斷失敗,那么接著你就應該檢查你最近對程序所作的修改。
如果你已經有一個大型程序,并且該程序存在托管內存泄漏問題,那么你應該使用windgb.exe工具來幫助你解決問題。當然你還可以使用其他的圖形化工具,比如CLR Profiler, SciTech的Memory Profiler,或者Red Gate的ANTS Memory Profiler。
弱引用
有時候,引用一個對GC而言是“隱形”的對象,并且對象保持活動狀態,這非常有用。這既是弱引用,它由System.WeakReference類實現。使用WeakReference,使用其構造器函數并傳入目標對象。
var sb = new StringBuilder ("this is a test");
var weak = new WeakReference (sb);
Console.WriteLine (weak.Target); // This is a test
如果目標對象僅僅由一個或多個弱引用所引用,那么GC會把其加入到垃圾回收隊列中。如果目的對象被回收,那么WeakReference的Target屬相則為NULL。
var weak = new WeakReference(new StringBuilder("weak"))
Console.WriteLine(weak.Target); // weak
GC.Collect();
Console.WriteLine(weak.Target == null); // (true)
為了避免目標對象在測試其為null和使用目標對象之間被回收,把目標對象分配給一個局部變量
var weak = new WeakReference (new StringBuilder ("weak"));
var sb = (StringBuilder) weak.Target;
if (sb != null) { /* Do something with sb */ }
一旦目標對象分配給一個局部變量,那么目的對象就有了一個強類型根對象,從而在局部變量使用期間不會被回收。
下面例子中的類通過弱引用追蹤所有被實例化的Widget對象,從而使這些實例不會被回收
class Widget { static List<WeakReference> _allWidgets = new List<WeakReference>(); public readonly string Name; public Widget (string name) { Name = name; _allWidgets.Add (new WeakReference (this)); } public static void ListAllWidgets() { foreach (WeakReference weak in _allWidgets) { Widget w = (Widget)weak.Target; if (w != null) Console.WriteLine (w.Name); } } }
這樣一個系統的唯一缺點就是,靜態列表會隨著時間推移而增加,逐漸累積對應null對象的弱引用。因此,你需要自己實現一些清理策略。
弱引用和緩存
使用弱引用的目的之一是為了緩存大對象圖。通過弱引用,使得耗費內存的數據可以進行簡要的緩存而不是造成內存的大量占用。
_weakCache = new WeakReference (...); // _weakCache is a field
...
var cache = _weakCache.Target;
if (cache == null) { /* Re-create cache & assign it to _weakCache */ }
在實際上,該策略只會發揮一半的作用,這是因為你不能控制GC何時運行,并且也不能控制GC會會執行哪一類回收。尤其是,當你的緩存是在Gen0中,那么這類內存會在微妙級別類被回收。因此,至少,你需要使用兩類緩存,通過它們,首先你擁有一個強類型,然后不
時地把該強類型轉換成弱類型。
弱引用和事件
在前面的章節中,我們看到事件是如何引起內存泄漏。而且解決這種內存泄漏的最簡單方法是避免時間訂閱,或者對為訂閱事件的對象實現Dispose方法。此外,弱引用也提供了另外一種解決方案。
假設一個帶來對其目標持有一個弱引用。那么這樣的一個代理并不會使其目標為活動狀態,除非這些目標對象有獨立的引用。當然,這并不會阻止一個被觸發的代理,在目標對象進入回收隊列之后但在GC開始對該目標對象執行回收前的時間段中,擊中一個未被引用的目
標。為了該方法高效,你的代碼必須非常穩定。下面的代碼就是就是采用這種方式的具體實現:
復制代碼
public class WeakDelegate<TDelegate> where TDelegate : class
{
class MethodTarget
{
public readonly WeakReference Reference;
public readonly MethodInfo Method;
public MethodTarget (Delegate d)
{
Reference = new WeakReference (d.Target);
Method = d.Method;
}
}
List<MethodTarget> _targets = new List<MethodTarget>();
public WeakDelegate()
{
if (!typeof (TDelegate).IsSubclassOf (typeof (Delegate)))
throw new InvalidOperationException
("TDelegate must be a delegate type");
}
public void Combine (TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
_targets.Add (new MethodTarget (d));
}
public void Remove (TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
{
MethodTarget mt = _targets.Find (w =>
d.Target.Equals (w.Reference.Target) &&
d.Method.MethodHandle.Equals (w.Method.MethodHandle));
if (mt != null) _targets.Remove (mt);
}
}
public TDelegate Target
{
get
{
var deadRefs = new List<MethodTarget>();
Delegate combinedTarget = null;
foreach (MethodTarget mt in _targets.ToArray())
{
WeakReference target = mt.Reference;
if (target != null && target.IsAlive)
{
var newDelegate = Delegate.CreateDelegate (
typeof (TDelegate), mt.Reference.Target, mt.Method);
combinedTarget = Delegate.Combine (combinedTarget, newDelegate);
}
else
deadRefs.Add (mt);
}
foreach (MethodTarget mt in deadRefs) // Remove dead references
_targets.Remove (mt); // from _targets.
return combinedTarget as TDelegate;
}
set
{
_targets.Clear();
Combine (value);
}
}
}
復制代碼
上述代碼演示了許多C#和CLR的有趣的地方。首先,我們在構造器中檢查了TDelegate是一個代理類型。這是因為C#本身的限制--因為下面的語句不符合C#的語法
... where TDelegate : Delegate // Compiler doesn't allow this
由于必須要進行類型限制,所以我們在構造器中執行運行時檢查。
在Combine方法和Remove方法中,我們執行了引用轉換,通過as運算符(而沒有使用更常見的轉換符)把target對象轉換成Delegate類型。這是由于C#不允許轉換符使用類型參數--因為它不能分清這是一個自定義的轉換還是一個引用抓換(下面的代碼不能拖過編譯)。
foreach(Delegate d in ((Delegate)target).GetInvocationList())
? ? ? ? ? ? ? ? _targets.Add(new MethodTarget(d));
當調用GetInvocationList,由于這些方法可能被一個多播代理調用,多播代理就是一個代理有多余一個的方法接收。
對于Target屬性,我們使其為一個多播代理--通過一個弱引用包含所有的代理引用,從而使其目標對象保持活動。然后我們清楚剩余的死引用,這樣可以避免_targets列表無限制的增長。下面的代碼演示了如何使用我們上面創建的實現了事件的代理類:
復制代碼
public class Foo
{
WeakDelegate<EventHandler> _click = new WeakDelegate<EventHandler>();
public event EventHandler Click
{
add { _click.Combine (value); } remove { _click.Remove (value); }
}
protected virtual void OnClick (EventArgs e)
{
EventHandler target = _click.Target;
if (target != null) target (this, e);
}
}
復制代碼
請注意,在觸發事件時,在檢查和調用之前,我們把_click.Target對象賦值給一個臨時變量。這就避免了目標對象被GC回收的可能性。
參考
http://msdn.microsoft.com/en-US/library/system.idisposable.aspx
========
關于C#中垃圾回收GC雜談
http://blog.csdn.net/pan869823184/article/details/19299581在初學階段用.Net編寫程序時,一直都未曾考慮過程序垃圾資源回收率的問題,那是因為老師老在課堂講什么不用管,不用理會,一聽到不用理會,好吧,從此寫程序就肆無忌憚的了!程序卡死、內存暴漲、順便偶爾來幾個內存錯誤,一看到這個就頭大了。現在想想,
課堂老師講的那句話,卻只聽進了前半句。。。
閑聊無事,也不用再怕什么在職防止泄露啥啥機密、啥啥技術的、、、嘎嘎、、、、(下面的純屬個人觀點,如有雷同、敬請繞道、、、)
在.Net里面垃圾收集的工作方式:
運行.NET應用程序時,程序創建出來的對象實例都會被CLR跟蹤,CLR都是有記錄哪些對象還會被用到(存在引用關系);哪些對象不會再被用到(不存在引用關系)。CLR會整理不會再被用到的對象,在恰當的時機,按一定的規則銷毀部分對象,釋放出這些對象所占用的
內存。
說到這里,那就引出了新的技術點:
CLR是怎么記錄對象引用關系的?
CLR會把對象關系做成一個“樹圖”,這樣標記他們的引用關系
CLR是怎么釋放對象的內存的?
關鍵的技術是:CLR把沒用的對象轉移到一起去,使內存連續,新分配的對象就在這塊連續的內存上創建,這樣做是為了減少內存碎片。注意!CLR不會移動大對象
垃圾收集器按什么規則收集垃圾對象?
CLR按對象在內存中的存活的時間長短,來收集對象。時間最短的被分配到第0代,最長的被分配到第2代,一共就3代。
一般第0貸的對象都是較小的對象,第2代的對象都是較大的對象,第0代對象GC收集時間最短(毫秒級別),第2代的對象GC收集時間最長。當程序需要內存時(或者程序空閑的時),GC會先收集第0代的對象,
收集完之后發現釋放的內存仍然不夠用,GC就會去收集第1代,第2代對象。(一般情況是按這個順序收集的)
如果GC跑過了,內存空間依然不夠用,那么就拋出了OutOfMemoryException異常。
GC跑過幾次之后,第0代的對象仍然存在,那么CLR會把這些對象移動到第1代,第1代的對象也是這樣。
既然有了垃圾收集器,為什么還要Dispose方法和析構函數?
因為CLR的緣故,GC只能釋放托管資源,不能釋放非托管資源(數據庫鏈接、文件流等)。
那么該如何釋放非托管資源呢?
一般我們會選擇為類實現IDispose接口,寫一個Dispose方法。
讓調用者手動調用這個類的Dispose方法(或者用using語句塊來自動調用Dispose方法)
Dispose執行時,析構函數和垃圾收集器都還沒有開始處理這個對象的釋放工作
有時候,我們不想為一個類型實現Dispose方法,我們想讓他自動的釋放非托管資源。那么就要用到析構函數了。
析構函數是個很奇怪的函數,調用者無法調用對象的析構函數,析構函數是由GC調用的。
你無法預測析構函數何時會被調用,所以盡量不要在這里操作可能被回收的托管資源,析構函數只用來釋放非托管資源
GC釋放包含析構函數的對象,比較麻煩(需要干兩次才能干掉她),
CLR會先讓析構函數執行,再收集它占用的內存。
我們需要手動執行垃圾收集嗎?什么場景下這么做?
GC什么時候執行垃圾收集是一個非常復雜的算法(策略)
大概可以描述成這樣:
如果GC發現上一次收集了很多對象,釋放了很大的內存,
那么它就會盡快執行第二次回收,
如果它頻繁的回收,但釋放的內存不多,
那么它就會減慢回收的頻率。
所以,盡量不要調用GC.Collect()這樣會破壞GC現有的執行策略。
除非你對你的應用程序內存使用情況非常了解,你知道何時會產生大量的垃圾,那么你可以手動干預垃圾收集器的工作?
我有一個大對象,我擔心GC要過很久才會收集他,
[csharp] view plain copy print?在CODE上查看代碼片派生到我的代碼片
??
關于弱引用和垃圾收集之間的關系?
當一個大對象被使用后不存在引用關系時,GC就會自動回收它占用的內存。
當這個對象足夠大的情況下,GC在回收它時,可能時間稍微會長點,當用戶需要再次使用該對象時,我們可以從回收池中再次提取該對象,這里就涉及到弱引用,代碼如下:
[csharp] view plain copy print?在CODE上查看代碼片派生到我的代碼片
var bss = new BsCtl(BrowserContainer); ?
? ? ? ? ? ? var vbss = new WeakReference<BsCtl>(bss); ?
? ? ? ? ? ? bss = null; ?
? ? ? ? ? ? BsCtl ok; ? ? ? ? ? ? ?
? ? ? ? ? ? vbss.TryGetTarget(out ok); ?
? ? ? ? ? ? //如果沒有進行垃圾收集OK不會為NULL ?
? ? ? ? ? ? if (ok == null) ?
? ? ? ? ? ? { ?
? ? ? ? ? ? ? ? //如果已經進行了垃圾收集,就會執行這段代碼 ?
? ? ? ? ? ? ? ? ok = new BsCtl(BrowserContainer); ?
? ? ? ? ? ? } ?
垃圾收集隨時可以收集bss對象,
如果收集了,就會進入if語句塊,如果沒有收集,就不會進入if語句塊,TryGetTarget(out ok)就成功把bss從垃圾堆里撈回來了。
注意:這里只說了短弱引用,沒有提及長弱引用,我覺得長弱引用使用的場景較少。
垃圾收集器優點:
因為我沒有很豐富的C/C++編程經驗,如果想談垃圾收集器的好處,那么勢必要和C/C++這樣的較低級的語言對比。所以一般性的回答都是減少內存使用不當的BUG,提升編程效率之類的問題。
========
垃圾收集器原理
http://www.tuicool.com/articles/Nbyqi2原文 ?http://www.cnblogs.com/izhaogang/p/collector.html
在編程的過程中,你是否遇到過OutOfMemeryException的異常?程序在做性能測試時,應用服務器程序消耗的內存不斷上升?在使用開源框架時,由于沒有及時的Dispose而導致程序的異常發生?而造成這些異常的原因都是沒有合理的釋放內存導致的。我們不經要問,C#
框架下不是有GC(自動垃圾收集器)嗎?那么為什么還會出現如此異常錯誤呢?GC到底何時執行,執行時又做了什么?GC對性能的影響?怎樣合理的釋放資源呢?下面我們來揭開垃圾收集器的神秘面紗。
1、垃圾收集平臺的基本工作原理
1.1 基本原理分析
我們知道,C#是CLR(Common Language Runtime公共語言運行庫)下的一種托管代碼語言,它的類型和對象在應用計算機內存時,大體用到兩種內存,一種叫堆棧,另一種叫托管堆。C#中主要分為值類型和引用類型,當聲明一個值類型對象時,會在棧中分配適當大小的
內存,內存空間存儲對象的值。其中維護一個棧指針,它包含棧中下一個可用內存空間的地址。當一個變量離開作用域時,棧指針向下移動并釋放變量所占用的內存,所以它任然指向下一個可用地址;當聲明一個引用類型對象時,引用變量也利用棧,但這時棧包含的只是
對另一個內存位置的引用,而不是實際的值。這個位置是托管堆中的一個地址,和棧一樣,它也維護一個指針,包含堆中下一個可用內存空間的地址。我們來寫一個簡單的事例代碼,看看它內部到底發生了什么?
namespace SourceDemo {class Program{ static void Main(string[] args){int iTotal = 1;Order order = new Order();}}class Order{} }
通過ILDASM.EXE工具查看對應的IL代碼如下:
.method private hidebysig static void Main(string[] args) cil managed {.entrypoint // Code size 10 (0xa).maxstack 1 .locals init ([0] int32 iTotal,[1] class SourceDemo.Order order)IL_0000: nopIL_0001: ldc.i4.1IL_0002: stloc.0IL_0003: newobj instance void SourceDemo.Order::.ctor()IL_0008: stloc.1IL_0009: ret } // end of method Program::Main
可以看到,聲明引用類型和值類型的區別在于引用類型有一個newObj創建對象的操作。那么newObj到底做了哪些操作呢?主要操作如下:
計算新建對象所需要的內存總數(包括基類的所有字段字節總數)。
在前面所得字節總數的基礎上再加上對象開銷所需的字節數。開銷包括:類型對象指針和同步塊的索引。
CLR檢查保留區域是否有足夠的空間來存放新建對象。
如果空間足夠,調用類型的構造函數,將對象存放在NextObjPtr指向的內存地址中。
如果空間不夠,就執行一次垃圾回收來清理托管堆,如果依然不夠,則拋出OutOfMemeryException異常
最后,移動NextObjPtr指向托管堆下一個可用地址。可以看到,垃圾收集器通過檢查托管堆上不再使用的對象來回收內存,那么垃圾收集器怎么確定對象是不再使用的對象呢?請接著往下看。
1.2 應用程序的根
每個應用程序都有一組根,一個根就是一個存儲對象,其中包含一個指向引用類型的內存指針,它或者指向托管堆的對象,或者被設為null。如類字段、方法參數或者是局部變量都是根,注意只有引用類型才被認為是根,而值類型只是占用內存永遠不會被認為為根。垃
圾收集器是怎么工作的呢?工作主要分為以下兩階段:
第一階段,標記對象階段。?
垃圾收集器開始執行的時候,首先假設托管堆中的對象都是可以收集的垃圾。它開始遍歷線程的堆棧檢查所有的根,如果發現根引用了一個對象那么就在該對象的同步塊的索引字段上設置一位來標記它。同時檢查該對象是否引用其他對象,如果引用則進行標記,通過遞
歸的方式進行標記,直到發現根及根引用的對象已經標記,垃圾收集器將繼續收集下一個根。
第二階段,壓縮階段。該階段垃圾收集器線性的遍歷堆以尋找包含未標記對象的連續區塊。如果垃圾收集器找到了較小內存塊,那么它忽略內存不計;如果找到了較大的連續內存塊,那么垃圾收集器將把內存中非垃圾對象搬移到這些連續內存塊中以壓縮托管堆。
圖:垃圾收集器執行前的托管堆。
對于以上描述,專業詞匯較多不是蠻好理解。我們來舉一個容易理解的例子:有一個執行清理房間的任務(任務方法),房間中有很多物品、柜子盒子及其里面的物品等都需要清理(對象清理),當我們執行這個任務時(調用方法),清理過程中我們可以標記物品,同
時可能存在這樣的情況,我們在清理其中一個盒子的時候,發現盒子里面還有其他的盒子,如手機盒子里面還有個裝充電器的盒子(手機里面又引用了手機充電器的對象),那么我們需要深度遍歷清理標記所有的盒子,遍歷完成后,我們會發現,有很多以前有用現在無
用的東西,如老式的手機充電器、數據線等;廢舊的電池等(不在使用,不可達對象);這樣我們會根據當時的情況將不再使用對象進行清理處理。而垃圾清理器大概就是做這樣的工作,只是它處理的方式及細節更加復雜。
1.3 對象的代
當CLR試圖尋找不可達對象的時候,它需要遍歷托管堆上的對象。隨著程序的運行,托管堆上的對象也越來越多,如果要對整個托管堆進行垃圾回收,那么會嚴重的影響性能。為了優化這個過程,CLR中使用了"代"的概念,托管堆上的每一個對象都被指定屬于某個“代”
(generation)。
托管堆上的對象可以分為0、1、2三個代:
0代:新構建的對象,垃圾收集器還沒對它們執行任何檢查
1代:在一次垃圾收集清理沒有被回收的對象
2代:在至少兩次垃圾收集清理沒有被回收的對象。
下面我們來看看CLR如何通過這種機制來優化垃圾收集機制的性能?
圖:垃圾收集代策略執行過程
CLR初始化時,它會為每一代選擇一個預算容量,假設為0代為256KB,1代為2M,2代為10M(實際可能不同),如果分配的新對象導致代容量超過預算容量,那么將執行垃圾收集清理操作。如上圖所示:
1、垃圾回收前,托管堆中對象ABCDE都處于第0代;
2、假設ABCDE已占用256K內存,當需要創建新對象F時,開始執行垃圾回收,垃圾收集器判斷CE為不可達對象,將對他們進行清理,完成后,對象ABD將變為1代對象;
3、現在需創建FGHIJ對象,它們將都處于0代;這個時候1代對象中的B可能不再被調用變為不可達對象。這里面會發現:當0代對象內存不超過256KB時,垃圾回收器不會對1代對象進行檢查清理,因此1代中不可達對象B在垃圾清理后依舊會保留在內存中。那么什么時候B會
被清理呢?
4、創建新對象,發現1代空間操作預算容量2M,這個時候將引發垃圾收集,回收不可達對象BH,同時原有1代對象AD變為2代,0代對象FGIJ變為1代。
下面我們來通過事例代碼驗證上述的執行步驟:
internal sealed class GenObj {
? ? ~GenObj()
? ? {
Console.WriteLine("Finalize GenObj");
? ? }
}
class Program
{
? ? static void Main(string[] args)
? ? {
Console.WriteLine("Maxnum gen:" + GC.MaxGeneration);
//創建一個新的對象
object o = new GenObj();
//因為是新創建的對象,為0代
Console.WriteLine("Gen "+GC.GetGeneration(o));
//執行垃圾收集提升對象的代
GC.Collect();
Console.WriteLine("Gen " + GC.GetGeneration(o));
//這里強制回收
GC.Collect();
Console.WriteLine("Gen " + GC.GetGeneration(o));
GC.Collect();
Console.WriteLine("Gen " + GC.GetGeneration(o));
o = null;
Console.WriteLine("Collecting Gen 0");
GC.Collect(0);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting Gen 0 1");
GC.Collect(1);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting Gen 0 1 2");
GC.Collect(2);
GC.WaitForPendingFinalizers();
Console.ReadLine();
? ? } ?
}
程序的返回結果為:
Maxnum gen: 2
Gen 0
Gen 1
Gen 2
Gen 2
Collecting Gen 0
Collecting Gen 0 1
Collecting Gen 0 1 2
Finalize GenObj
這里需要注意的時:
GC.Collect() ?強制對所有代碼進行即時回收
GC.Collect(int Generation) 強制對0代到指定代對象進行回收
因此:我們將代碼第二處的GC.Collect()修改為:GC.Collect(0)?
程序的返回結果則變為:
Maxnum gen: 2
Gen 0
Gen 1
Gen 1 ? ? //注意這里變為1 而不是2
Gen 2
Collecting Gen 0
Collecting Gen 0 1
Collecting Gen 0 1 2
Finalize GenObj
由此可以看出,垃圾回收機制通過引入代的機制,由遍歷整個托管堆對象變成遍歷少量的對象來達到性能優化的目的。當然,不僅如此,它還有其他的策略來進行性能優化。
策略1:根據回收存貨的比例高低來調整預算容量。如果垃圾收集器發現0代對象被收集以后存活下來的對象很少,它可能會決定將第0代的預算容量從256K減少到128K。已分配空間的減少意味著垃圾收集器執行的頻率更高,但每次收集工作會減少,這樣一來進程的工作集
會變小;如果發現0代對象被手機以后存貨下來的對象很多,也就是說沒有回收較多的內存,那么可能決定將預算容量從256K增加到512K,這樣回收的頻率降低,執行回收的內存較多。
策略2:大對象回收特殊機制。大對象是指任何占用內存等于或超過85000字節的對象,將會總被認為為2代對象。原因是:該堆中的對象的終結和內存釋放和小對象相同,但是它永遠不會被壓縮。因為將85000字節的內存塊搬移到堆中要浪費很多的CPU時間。
對于這樣的場景,我們來舉一個更易懂的例子,一個公司業績下滑,需要通過裁員來減清負擔,開始時決定全公司范圍裁員,結果搞的人心惶惶,人人自危,極大的影響了員工士氣和工作效益(就好比遍歷整個堆棧導致性能不佳);然后公司管理層決定優化這個方案,
將裁員的人員定為剛進公司1年的新員工(0代),因為這樣有一定的好處,不僅人少執行效率快,賠的錢少而且對業務影響也較小;經過這次風波后(資源清理),隨著市場行情的提升,業績越來越好,結果又持續招人(新對象創建),但是過了1年,由于XXX原因,效
益大幅下降(內存、性能等下降),又要開始裁員(引發系統清理),規則還是按照上一次的規則,只是之前上一年沒有裁掉的新員工(0代),他們在今年不再是一年級的新員工(上升為1代),在裁員的時候,發現只是裁新員工(部分表現不佳的)還不夠,那么需要
對去年新員工(1代)表現差的進行裁員(0代超過預算容量則開始檢查1代)。這個例子可能不是很符合現實,但是它可以體現出代的思想。
1.4 小結及啟發
通過上面的介紹,我們來回顧總結一下。我們可以學習到什么?我們大概能知道垃圾收集器是如何工作的,是如何高性能的工作的。我們不僅要會使用它,我們還需要知道它的原理是什么,這樣當你遇到它,你就不會覺得它有多么的神秘,不僅如此,更重要的是,里面
有很多思想是我們可以借鑒的,因為這些思想都是行內權威人士智慧的結晶。通過了解,我們還可以舉一反三學習到:
程序中根的標記遞歸算法,這不正是深度優先的算法嗎?這個算法在很多場景都在使用,比如說搜索中的爬蟲程序、圖的遍歷、最優最快路徑等等應用場景都會用到;垃圾收集器通過引進“代”的概念來進行性能優化的策略原理。我們在項目的開發過程中,也有很多應
用場景都可以借鑒這種思路來進行性能優化。比如說:現在很多大并發場景如12306、秒殺、購物網站等使用的排隊機制,它們可以智能的設置隊列容量,這不正是很好的體現嗎?在比如說多級緩存系統系統緩存策略等等,這些應用場景都可以借鑒該思想。
2、資源清理Finalize、Dispose、Using用法說明
2.1 Finalize使用及原理說明
終結(Finalization)是CLR提供的一種機制,他允許對象在垃圾回收其內存之前執行一些清理工作,回收它占用的資源(內存、本地資源等),當垃圾收集器判定一個對象為可收集垃圾時,它會通過該對象的Finalize方法來執行清理。C#中是通過在類名稱前加一個波浪
線~來定義的,這也就是我們所說的析構函數。通過ILDASM.EXE工具查看上面跟部分的GenObj類,打開確實可以發現Finalze方法。那么我們來看看Finalize的工作原理是什么?
.method family hidebysig virtual instance void?
? ? Finalize() cil managed
{
// Code size 25 (0x19)
.maxstack 1
.try
{
IL_0000: nop
IL_0001: ldstr "Finalize GenObj"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: nop
IL_000d: leave.s IL_0017
} // end .try
finally
{
IL_000f: ldarg.0
IL_0010: call instance void [mscorlib]System.Object::Finalize()
IL_0015: nop
IL_0016: endfinally
} // end handler
IL_0017: nop
IL_0018: ret
} // end of method GenObj::Finalize
可以看到方法體的代碼在Try中生成,而base.Finalize的調用則在finally中。通常Finalize方法的實現時調用Win32的CloseHandle函數,該函數接受本地資源的句柄作為參數。如:FileStream類定義了一個文件句柄字段來標示本地資源,同時也定了一個Finalize方法,
該方法內部調用CloseHandler函數并為它傳遞文件句柄作為參數,確保托管堆的FileStream對象成為可收集垃圾之前,本地文件句柄可以得到關閉。C#中也提供給了相應的類來進行非托管資源的清理類:
CriticalFinalizerObject類型。
它位于命名空間System.Runtime.ConstrainedExecution。CLR賦予它三個很酷的特征:1、首次構造派生于它的類型的任何對象,CLR立即對繼承的層次結構中的所有Finalize方法進行JIT編譯,這樣在內存較小的情況下,不會影響Finalize方法因為沒有內存而無法執行,
從而導致資源泄露。2、CLR在調用了非派生自CriticalFinalizerObject該類的Finalize方法后再調用派生于CriticalFinalizerObject類型的對象的Finalize方法。這樣可以確保擁有Finalize方法的托管資源類可以在Finalize方法中訪問派生自CriticalFinalizerObject
的對象。3、應用程序域被非法中斷時,可以確保CLR調用派生自CriticalFinalizerObject類型的Finalize方法來執行資源清理。
SafeHandle類型及其派生類型。
Microsoft意識到最常用的本地資源就是有Windows提供的資源,而且Windows資源都是由句柄操作。為了使便車簡單,因此提供了SafeHandle類來提供資源句柄操作,它位于命名空間System.Runtime.InteropService.它本身派生于對于CriticalFinalizerObject類型,對
于這個類的用法,具體可以查閱相關資料。
我們來看看SafeHandle類都作了什么?
[SecurityCritical, __DynamicallyInvokable, SecurityPermissio(SecurityAction.InheritanceDemand, UnmanagedCode=true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
{
// Fields
private bool _fullyInitialized;
private bool _ownsHandle;
private int _state;
[ForceTokenStabilization]
protected IntPtr handle;
// Methods
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle)
{
this.handle = invalidHandleValue;
this._state = 4;
this._ownsHandle = ownsHandle;
if (!ownsHandle)
{
GC.SuppressFinalize(this);
}
this._fullyInitialized = true;
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), SecurityCritical, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public void Close()
{
this.Dispose(true);
}
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecurityCritical, __DynamicallyInvokable]
public extern void DangerousAddRef(ref bool success);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public IntPtr DangerousGetHandle()
{
return this.handle;
}
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
public extern void DangerousRelease();
[SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public void Dispose()
{
this.Dispose(true);
}
[SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
this.InternalDispose();
}
else
{
this.InternalFinalize();
}
}
[SecuritySafeCritical, __DynamicallyInvokable]
~SafeHandle()
{
this.Dispose(false);
}
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private extern void InternalDispose();
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private extern void InternalFinalize();
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
protected abstract bool ReleaseHandle();
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
protected void SetHandle(IntPtr handle)
{
this.handle = handle;
}
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
public extern void SetHandleAsInvalid();
// Properties
[__DynamicallyInvokable]
public bool IsClosed
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries"), __DynamicallyInvokable]
get
{
return ((this._state & 1) == 1);
}
}
[__DynamicallyInvokable]
public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable] get; }
}
首先可以看到,它繼承于CriticalFinalizerObject對象,這樣讓它具備上面提到的三個特性;它同時實現了IDisposable接口。然后通過Dispose和close釋放托管資源和非托管資源。其中提供了對本地資源句柄的操作,ReleaseHandle 如果在派生類中重寫,執行釋放句
柄所需的代碼。更多細節可以查看, https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.safehandle.aspx
哪些時間會導致Finalize方法的調用呢?
0代對象充滿 該事件是目前導致垃圾回收執行最常見的一種方式。
代碼顯式調用GC.Collect()方法 Micrsoft強烈建議不要這樣干,但某些時候執行還是有意義的。
Windows報告內存不足
CLR卸載應用程序域
CLR被關閉
由此可以看出Finalize方法的執行不能顯式調用,因此它執行時間具備不確定性。
2.2 Dispose使用
Finalize方法非常有用,它可以確保托管對象在釋放內存的同時不會泄露本地資源,但是它的問題在于我們不知道何時才會調用它。在使用本地資源的托管類型時,能夠確定的釋放或者是關閉對象都是很有用的。要提供確定釋放或者關閉對象的能力,一個類型通常需要
實現一種釋放模式(DisposePattern).通過前面的SafeHandler類可以顯式關閉本地資源,這是由于它實現了IDisposable接口。我們來看一下MSDN給出的Dispose釋放寫法。
using System;
class BaseClass : IDisposable
{
//Flag: Has Dispose already been called?bool disposed = false;
// Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(true);
//調用GC.SuppressFinalize(this)方法來阻止Finalize方法的調用
GC.SuppressFinalize(this);
}
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;?
if (disposing) {
// 釋放托管資源
//
}
// 釋放非托管資源
//設置true 表示對象正在被顯式的執行資源清理而不是垃圾收集器執行終結操作
disposed = true;
}
}
通過Dispose來釋放資源,其實只是清理SafeHandle對象包裝的資源方式之一。SafeHandle包裝的資源清理還可以通過編程人員顯式的調用Close、Dispose方法來清理;或者是通過垃圾收集器調用對象的Finalize方法來釋放。上面給出的SafeHandle類代碼實現,正好可以
說明這一點。
2.3 Using使用
前面介紹了怎樣顯示的調用一個類型的Dispose或者是Close方法,如果決定顯式調用,那么強烈建議把他們放在一個異常處理的finally代碼塊中,這樣可以保證它們被執行。但是這樣做書寫的代碼很是繁瑣。為了解決這個問題,C#提供了一個using語句,它簡化了上述
finally的操作,并且能夠得到和上述一樣的效果。
3、實例分析
3.1 Windows服務關閉問題
在很多時候,我們需要編寫并開啟一個Windows服務來執行需要循環執行的應用需求,在開啟任務后,程序順利執行;當關閉Windows服務時,發現服務關閉了,但是服務對應的資源線程還沒有完全結束,要經過一段時間后,服務才會完全停止。這很有可能是因為,在服
務Stop()方法里面,沒有完全釋放程序調用的資源導致的。
3.2 Redis使用遇到的問題
記得以前在使用Redis過程中遇到了一個奇怪的問題。
先附上Redis幫助類:RedisManager.cs
internal class RedisManager
{
private static readonly PooledRedisClientManager _Manager;
static RedisManager()
{
_Manager = GetManager();
}
public static PooledRedisClientManager Manager
{
get
{
return _Manager;
}
}
#region Help Methods
private static PooledRedisClientManager GetManager()
{
var conn = ConfigurationManager.ConnectionStrings["Redis"].ConnectionString;
if (string.IsNullOrEmpty(conn))
{
throw new Exception("請配置ConnectionString Key 為Redis的連接串");
}
var manager = new PooledRedisClientManager(conn);
return manager;
}
#endregion
}
Dao.cs 數據操作基類
public abstract class Dao<TEntity> : IDisposable where TEntity : class
{
private IRedisClient _Client;
private IRedisTypedClient<TEntity> _Collection;
public Dao()
{
_Client = RedisManager.Manager.GetClient();
_Collection = _Client.As<TEntity>();
}
public void Save(TEntity entity)
{
_Collection.Store(entity);
}
public TEntity Get(object id)
{
return _Collection.GetById(id);
}
public void Delete(Object id)
{
_Collection.DeleteById(id);
}
#region Dispose
~Dao()
{
_Client.Dispose();
}
/****以下為修改BUG時新加****/
/*public void Close()
{
_Client.Dispose();
}
public void Dispose()
{
_Client.Dispose();
}*/
#endregion
}
單元測試用例:
[TestMethod]
public void TestAddAndGet()
{
for (var i = 0; i < 10; i++)
{
//Bug前代碼
dao.Save(new TestEntity() { Id = "fdsafsa", FirstName = "Jack", SecondName = "Cui" });
Assert.AreEqual(dao.Get("fdsafsa").FirstName, "Jack");
//修復Bug代碼
/*using (var dao = new TestDao())
{
dao.Save(new TestEntity() { Id = "fdsafsa", FirstName = "Jack", SecondName = "Cui" });
Assert.AreEqual(dao.Get("fdsafsa").FirstName, "Jack");
}*/
}
}
運行單元測試,單條保存測試用例通過。但是在應用程序大量數據操作測試的時候,發現寫入Redis的數據有丟失的情況,但并不是每次都會丟失。并且系統沒有操作失敗的異常日志。這個時候就感覺特別奇怪,于是就在單元測試時想辦法重現這個錯誤,當按上面測試用
例,循環10次保存數據測試用例通過,當循環100次時,發現異常重現了。沒有異常拋出,測試用例也沒有返回。然后設置斷點調試,發現在運行第11次時,系統在_Client = RedisManager.Manager.GetClient()此處停住了??戳艘幌?#xff0c;代碼中用到了PooledRedis?
ClientManager 客戶端池對象,池對象有個特點是,有一個池的容量,當容量滿的時候需要等待。而現在池中保存的就是RedisClient,是否是由于RedisClient達到了使用上限導致的。那么我們手動釋放一下RedisClient是否解決這個錯誤,馬上嘗試了一下,通過使用
Using顯式釋放資源,發現確實解決了問題。問題是解決了,但是我們不經要問?系統中不是有析構函數嗎?難道這是PooledRedisClientManager池的一個Bug嗎?帶著這樣的疑問,讓我們來查看一下問題到底出現在哪里?
1、析構函數只有在GC進行垃圾收集時才會被調用,而GC并不會馬上執行,執行時間是不確定的。
2、那么這到底是不是PooledRedisClientManager池的一個Bug呢?
查看了一下對應的源碼:
protected readonly int PoolSizeMultiplier = 10;
public IRedisClient GetClient()
{
lock (writeClients)
{
AssertValidReadWritePool();
RedisClient inActiveClient;
while ((inActiveClient = GetInActiveWriteClient()) == null)
{
if (PoolTimeout.HasValue)
{
// wait for a connection, cry out if made to wait too long
if (!Monitor.Wait(writeClients, PoolTimeout.Value))
throw new TimeoutException(PoolTimeoutError);
}
else
Monitor.Wait(writeClients, RecheckPoolAfterMs);
}
WritePoolIndex++;
inActiveClient.Active = true;
InitClient(inActiveClient);
return inActiveClient;
}
}
從源代碼可以看出,在獲取可用GetInActiveWriteClient()為null時,有一個循環調用,線程一直等待獲取可以用的RedisClient.當池中無可用的RedisClient對象時,那么線程將一直等待。對象池有一個特征:獲取池中對象 → 使用對象 → 歸還對象 。那么是否是由
于使用完對象后沒有歸還對象呢?通過Using釋放使用的對象切實可以起到歸還的效果。于是再去挖掘程序中是否有Dispose或者是歸還對象的操作, 發現該池定義了protected void Dispose(RedisClient redisClient)的方法,但是瀏覽源碼切實沒有發現任何地方顯式
調用這個Dispose。
3.3 應用程序線程使用內存不斷上升
記得之前同事遇到這樣的一個BUG,系統上線后,監控發現該應用程序使用內存不斷上升,這個不得了,這意外著隨著時間的持續,系統會因為內存不足導致應用掛掉。于是通過獲取線上的DUMP文件,通過分析,發現char[] 數組的對象特別多,那么在什么時候我們會使
用這么多的char[]呢?回顧一下,好像沒有直接使用char[]的地方,但是我們知道,string對象經過編譯后,它就是有char[]組成的,我們試著去找是否有StringBulider對象不停地加入數據,但是沒有執行清理。結果真的發現有如此一個對象,這個對象據說是用來進行
測試調試使用的,上線的時候應該去掉,結果上線的時候忘記了。
參考資料
《框架設計 CLR Via C#》 Jeffrey Richter著
結束語:
在技術學習的過程中,很多時候我們知其然不知其所以然,因此在開發的過程中可能遇到不知其所以然而導致的問題,到最后也無法找到問題的根本原因。我們需要深入了解原理,并且通過原理舉一反三,在其他類似的應用場景可以借鑒他們的思想。
寫此文主要有三方面的目的:
1、將學過的東西通過文字的形式分享出來,一直被分享,從未進行分享。- -
2、有些時候,很多東西我們可能都理解,但是很難系統的書寫出來,書寫出來可以對已學知識和個人理解做一個記錄和總結,進一步鞏固已學知識。
3、試著將比較枯燥的概念和理論通過更通俗易懂的例子解釋出來,同時能夠將這些枯燥難解的理論和實際結合,讓知識體現的更加具體一點。
個人感覺,文章還沒有達到個人預期效果。其主要表現在如下兩方面:其一,對于細節的理解可能說的不夠透徹,沒有找到通俗易懂的例子來進行解釋;其二,對于項目中遇到的關于垃圾收集典型問題所舉例子還不夠豐富,沒有真正體現出核心的價值。因此歡迎各位博
友能夠分享個人經驗進行補充,讓對此方面知識還不是十分了解的同學更容易理解。
========
淺談.NET垃圾回收機制
? ? ? ? 垃圾收集器(GarbageCollection)是組成.Net平臺一個很重要的部分,.NET垃圾回收機制降低了編程復雜度,使程序員不必分散精力去處理析構。不妨礙設計師進行系統抽象。減少了由于內存運用不當產生的Bug。成功的將內存管理工作從程序的編寫時,脫離至運行時的優點。
方法/步驟
1
?關于垃圾回收
?
? ? ? 在.NET Framework中,內存中的資源(即所有二進制信息的集合)分為"托管資源"和"非托管資源".托管資源必須接受.NET Framework的CLR(通用語言運行時)的管理(諸如內存類型安全性檢查),而非托管資源則不必接受.NET Framework的CLR管理. 需要手動清理垃圾(
顯式釋放)。
?
? ? ? 托管資源在.NET Framework中又分別存放在兩種地方: "堆棧"和"托管堆"(以下簡稱"堆");規則是,所有的值類型(包括引用和對象實例)和引用類型的引用都存放在"堆棧"中,而所有引用所代表的對象實例都保存在堆中。在C#中,釋放托管資源是可以自動通過"垃圾回
收器"完成的(注意,"垃圾回收"機制是.NET Framework的特性,而不是C#的).
?
? ? ? 在C++時代,我們需要自己來管理申請內存和釋放內存. 于是有了new, delete關鍵字. 還有的一些內存申請和釋放函數(malloc/free). C++程序必須很好地管理自己的內存, 不然就會造成內存泄漏(Memory leak). 在.net時代, 微軟為開發人員提供了一個強有力的
機制--垃圾回收. 垃圾回收機制是CLR的一部分, 我們不用操心內存何時釋放, 我們可以花更多精力關注應用程序的業務邏輯. CLR里面的垃圾回收機制用一定的算法判斷某些內存程序不再使用,回收這些內存并交給我們的程序再使用.
2
?垃圾回收的功能
?
? ? ?1、用來管理托管資源和非托管資源所占用的內存分配和釋放。
?
? ? ?2、尋找不再使用的對象,釋放其占用的內存, 以及釋放非托管資源所占用的內存。
?
? ? ?3、垃圾回收器釋放內存之后, 出現了內存碎片, 垃圾回收器移動一些對象, 以得到整塊的內存,同時所有的對象引用都將被調整為指向對象新的存儲位置。
3
?回收內存的模式
?
? ? ?在.net中提供三種模式來回收內存資源:dispose模式,finalize方法,close方法。
?
? ? ?1、dispose提供了一種顯示釋放內存資源的方法。dispose調用方法是:要釋放的資源對象.dispose().
?
? ? ?2、finalize方法是.net的內部的一個釋放內存資源的方法。這個方法不對外公開,由垃圾回收器自己調用。
?
? ? ?3、close和dispose其實一樣,只不過有的對象沒有提供dispose的方法,只提供了close方法,而close其實在那個對象的類中,依然是調用了一個私有的dispose方法,而finalize其實也是調用一個不對外公開的dispose方法。
4
?回收一般過程
?
? ? ?1、垃圾回收時機:托管堆滿了,內存分配即將不足時,0代內存分配滿了,或其他情況,微軟沒有公開該部分算法。程序員可以手動調用GC.Collect(),但是會有警告,微軟并不建議這么做。
?
? ? ?2、垃圾確認:通過根來尋找可達的對象(以后添加),并做標記,然后回收沒有標記的對象。
?
? ? ?3、垃圾回收:內存回收,對于實現了Finalize方法的對象請參考最上面1的介紹。
?
? ? ?4、內存轉移,合并。垃圾回收后使得內存不連續,零碎,.Net會將利用的內存合并為連續的塊,然后更新對象的指針。
5
?注意的地方
?
? ? ?1、值類型(包括引用和對象實例)和引用類型的引用其實是不需要什么"垃圾回收器"來釋放內存的,因為當它們出了作用域后會自動釋放所占內存(因為它們都保存在"堆棧"中,學過數據結構可知這是一種先進后出的結構);
? ? ?2、只有引用類型的引用所指向的對象實例才保存在"堆"中,而堆因為是一個自由存儲空間,所以它并沒有像"堆棧"那樣有生存期("堆棧"的元素彈出后就代 表生存期結束,也就代表釋放了內存),并且非常要注意的是,"垃圾回收器"只對這塊區域起作用;?
? ? ?3、"垃圾回收器"也許并不像許多人想象的一樣會立即執行(當堆中的資源需要釋放時),而是在引用類型的引用被刪除和它在"堆"中的對象實例被刪除中間有 個間隔,為什么呢? 因為"垃圾回收器"的調用是比較消耗系統資源的,因此不可能經常被調用!(當然,用戶代碼
可以用方法System.GC.Collect()來強制執行"垃圾回收器")
? ? 4、有析構函數的對象需要垃圾收集器兩次處理才能刪除:第一次調用析構函數時,沒有刪除對象,第二次調用才真正刪除對象。
? ? 5、由于垃圾收集器的工作方式,無法確定C#對象的析構函數何時執行。
? ? 6、可實現IDisposable接口的Dispose()來顯示釋放由對象使用的所有未托管資源。
? ? 7、垃圾收集器在釋放了它能釋放的所有對象后,就會壓縮其他對象,把他們都移動回heap的端部,再次形成一個連續的塊。
========
總結
以上是生活随笔為你收集整理的C#垃圾回收学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 灾难恢复学习总结
- 下一篇: VS 断点无法调试学习总结