.NET對象清理:垃圾回收和資源清理
原創(chuàng)【51CTO.com原創(chuàng)稿件】在 .NET 中垃圾回收和資源清理是重中之重的內(nèi)容,也是所有程序都必須用到的機制,但是有很大一部分開發(fā)人員并不知道垃圾回收和資源清理的原理。那么,我將通過這篇文章向各位讀者詳細講解一下垃圾回收和資源清理。
一、垃圾回收
.NET中垃圾回收是運行時的核心功能,它的作用是回收不再被引用的對象所占用的內(nèi)存。這里我們要注意垃圾回收器只回收內(nèi)存資源而不處理其他資源。此外垃圾回收器是根據(jù)是否存在任何引用來決定要清理那些東西,也就是說垃圾回收器處理的是不被引用的引用對象,并且只能回收堆上的內(nèi)存。
-
簡述 在 .NET 中垃圾回收的很多細節(jié)都和 CLI 有關(guān),我們常用的 Microsoft.NET 框架中實現(xiàn)垃圾回收的算法是 mark-and-compact 算法 。當每次一次垃圾回收周期開始時,它會查找對象的所有根引用(一般來說根引用來自靜態(tài)變量、CPU寄存器和局部變量或參數(shù)實例的任何引用)?;诓檎业降乃懈?,垃圾回收器就可以遍歷每個根引用標識的樹形結(jié)構(gòu),并遞歸確定每個根引用指向的對象,進而識別出所有可達對象。
當執(zhí)行垃圾回收時,垃圾回收器會將所有可達對象一個挨一個的放在一起,這樣就可以覆蓋不可達對象所占用的內(nèi)存。為了定位和移動可達對象,進程中所有托管線程都會在垃圾回收期間暫停運行,這樣就可以保證垃圾回收器在運行期間維持狀態(tài)一致性。雖然這么做會造成應(yīng)用程序短暫停止工作,但是一般來說只要垃圾回收周期不是特別長,這個短暫的停止工作是很難發(fā)覺的。在我們開發(fā)時有時可能不希望在運行一些代碼段時執(zhí)行垃圾回收,這時我們可以在代碼段之前使用 System.GC 對象所包含的 Collect 方法來讓垃圾回收暫時跳過這些代碼。當然這么做是不會阻止垃圾回收運行的,只是減少了這部分代碼可能被回收的概率,但是這里有一個前提條件:代碼段執(zhí)行期間不會發(fā)生內(nèi)存被大量消耗使用的情況。在 .NET 中垃圾回收有一個特別的地方,就是并非所有的垃圾都會在一個垃圾回收周期內(nèi)被回收。這是為什么呢?因為在 .NET 垃圾回收器中有一個名字叫 generation 的概念,翻譯成中文就是 代 。它會清理那些生存時間較短的對象,那些在一次垃圾回收周期中存活下來的對象會降低清理頻率。也就是說當一個對象在一次垃圾回收周期中存活下來,那么它將會被移動到下一代中,如果它又在一次垃圾回收周期中存活下來,那么它將被移動到最后一代,也就是第二代(為什么是第二代呢?因為 .NET 垃圾回收機制中代是從 0 開始的),第零代清理速度最快,第二代清理速度最慢。 -
弱引用 弱引用這個名詞很少有開發(fā)人員聽過,所謂的弱引用是為創(chuàng)建起來開銷很高并且維護成本也很大的對象而設(shè)計的。它不阻止垃圾回收器對對象的回收,但會維持一個引用,進而可以在被垃圾回收器回收之前可以重用。例如我們從數(shù)據(jù)庫中查詢一個龐大的數(shù)據(jù)列表向用戶展示,如果沒有使用弱引用當用戶關(guān)閉了這個列表,那么垃圾回收器就有很大可能將它回收,那么當用戶再次查看這個列表時,程序又需要從數(shù)據(jù)庫查詢并加載出來,這種操作成本是很高昂的。如果使用了如引用,每次請求列表時代碼首先檢查列表是否被清除,如果沒有被清除就直接將列表展示給用戶,如果被清除了就從數(shù)據(jù)庫查詢并展示給用戶,這就相當于對象在內(nèi)存中進行了緩存。如果開發(fā)人員認為對象應(yīng)該進行弱引用,那么就可以把這個對象賦值給 System.WeakReference 。下面我們來看一個弱認證的簡單例子:
- WeakReference Data;
- public FileStream Date()
- {
- FileStream fs= (FileStream)Data.Target;
- if(data!=null)
- {
- return data;
- }
- // more code
- Data.Target=data;
- return data;
- }
上面的代碼是一個標準的創(chuàng)建弱引用的代碼,我們可以看到在代碼中對變量 data 進行了 null 判斷,我們可以通過這個判斷來檢查垃圾回收器是否將其回收。這里還有一個關(guān)鍵代碼 FileStream fs= (FileStream)Data.Target; 這里將弱引用賦值給了強引用,這樣可以避免在檢查 null 后和訪問數(shù)據(jù)前,發(fā)生垃圾回收器回收弱引用。
二、資源清理
在前面一小節(jié)開頭我們說過垃圾回收之回收內(nèi)存中的對象,那么如果我們需要回收其他資源呢,例如數(shù)據(jù)庫連接、句柄、外部設(shè)備。這時我們就需要用到資源清理。
終結(jié)器 終結(jié)器是一個允許開發(fā)人員通過代碼來清理類資源的東西。終結(jié)器最大的特征是它不能在代碼中顯式調(diào)用,只有垃圾回收器負責對對象的實例調(diào)用終結(jié)器,因此開發(fā)人員無法在編譯時確定終結(jié)器在何時執(zhí)行,只能夠確定終結(jié)器時對象中最后一次被調(diào)用的地方。 終結(jié)器的定義也很簡單,只需要在類名之前加一個 ~ 符號即可。
- class Demo
- {
- public Demo(string name)
- {
- //more code
- }
- ~Demo()
- {
- Close();
- }
- public void Close()
- {
- //more code
- }
- //more code
- }
上述代碼我們就定義了一個簡單的終結(jié)器,我們定義終結(jié)器的時候需要注意以下四點:
因為終結(jié)器是在自己的線程中執(zhí)行的,因此如果終結(jié)器中存在一個未處理的異常就會很難診斷發(fā)現(xiàn),因為造成異常的情況并不清晰透明。所以我們必須避免在終結(jié)器中引發(fā)異常。
- 終結(jié)器是不允許傳遞任何參數(shù)的,也不能重載它;
- 因為它是被垃圾回收器所調(diào)用,因此給終結(jié)器加上訪問修飾符是毫無意義的;
- 如果父類中存在終結(jié)器,那么將會作為子類終結(jié)器的一部分被自動調(diào)用;
- 終結(jié)器必須顯示的釋放資源。
-
using 雖然終結(jié)器可以幫助我們在忘記顯式調(diào)用必要清理代碼的時候執(zhí)行清理,但是因為終結(jié)器的運行存在不確定性,因此我們只能將它作為備用機制。正常情況下我們可以使用 using 。 C# 中的 IDisposable 接口的 Dispose 方法為我們提供了實現(xiàn)細節(jié)。我們先來看一段代碼。
- class Demo
- {
- MyFileStream fs =new myFileStram();
- //more code
- fs.Dispose();
- //more code
- }
- class MyFileStream:IDisposable
- {
- public MyFileStream(string path)
- {
- //more code
- }
- //more code
- ~MyFileStream
- {
- Dispose(false);
- }
- public void Close()
- {
- Dispose();
- }
- public void Dispose()
- {
- Dispose(true);
- System.GC.SuppressFinalize();
- }
- public void Dispose(bool para)
- {
- // more code
- }
- }
上述代碼中我們顯式調(diào)用了 MyFileStream 類的 Dispose 方法。 Dispose 方法主要用來清理已經(jīng)用過的資源,但是這里存在一個問題,當我們調(diào)用 Dispose 方法時有可能會發(fā)生異常,這時我們就無法正確調(diào)用 Dispose 方法了,為了避免這個問題我們需要加入 try..finally 塊。但是我們無法保證開發(fā)人員每次都會寫 try...finally ,這時我們可以使用 C# 提供的 using 語句,我們將上面的調(diào)用代碼修改一下:
- class Demo
- {
- using(MyFileStream fs =new myFileStram())
- {
- //more code
- }
- }
這段代碼最終生成的 CIL 代碼和使用 try...finally 塊生成的代碼完全一樣。
-
垃圾回收、終結(jié)和 IDisposable 在上一小節(jié)的代碼中我們看到在 Dispose 方法中我們調(diào)用了 System.GC.SuppressFinalize(); ,它的作用是從終結(jié)隊列中移除 MyFileStream 實例。因為所有清理都在Dispose 方法中完成了,而不是等著終結(jié)器執(zhí)行。如果不調(diào)用 System.GC.SuppressFinalize() 方法實例將會一直在終結(jié)隊列中,只有當終結(jié)方法被調(diào)用之后才能在垃圾回收器中被回收,那么這就造成了托管資源垃圾回收處理時間的延遲。 Dispose 方法中調(diào)用了 Dispose(bool para) 方法,在這個方法里我們可以清理資源并阻止終結(jié)器。其次,我們定義了 Close 方法來調(diào)用 Dispose(bool para) 方法,這樣終結(jié)器就可以調(diào)用 Dispose(bool para) 方法來關(guān)閉釋放資源。針對前一小結(jié)的代碼需要有如下幾點注意:
在某些特殊情況下垃圾回收的對象有可能會被無意中重新引用一個待終結(jié)的對象。這樣,被重新引用的對象就不再是不可訪問的,所以不能當作垃圾被回收掉。假如對象的終結(jié)方法已經(jīng)運行,那么除非顯式標記為要進行終結(jié),否則終結(jié)方法不一定會再次運行。
- 只針對開銷大,成本高的對象實現(xiàn)終結(jié)器;
- 如果類存在終結(jié)器那么就必須實現(xiàn) IDisposable ;
- 不要在終結(jié)器中拋出異常;
- 在 Dispose 方法中必須調(diào)用 System.GC.SuppressFinalize ;
- 保證 Dispose 可以被重用;
- 保證 Dispose 方法的簡單性;
- 不能在終結(jié)器中調(diào)用未被終結(jié)的其他對象;
- 如果父類存在終結(jié)器,再重寫時必須調(diào)用父類終結(jié)器;
- 調(diào)用 Dispose 方法之后,將對象設(shè)為不可用。
三、小結(jié)
作者介紹:
朱鋼,筆名喵叔,國內(nèi)某技術(shù)博客認證專家,.NET高級開發(fā)工程師,7年一線開發(fā)經(jīng)驗,參與過電子政務(wù)系統(tǒng)和AI客服系統(tǒng)的開發(fā),以及互聯(lián)網(wǎng)招聘網(wǎng)站的架構(gòu)設(shè)計,目前就職于一家初創(chuàng)公司,從事企業(yè)級安全監(jiān)控系統(tǒng)的開發(fā)。
【51CTO原創(chuàng)稿件,合作站點轉(zhuǎn)載請注明原文作者和出處為51CTO.com】