如何在.net應用中發現和避免內存和資源泄露

如何在.net應用中發現和避免內存和資源泄露

By Fabrice Marguerie

儘管很多人相信在.net應用中談及內存及資源泄露是件很輕鬆的事情。但GC(垃圾回收器)並不是魔法師,並不能把你完全從小心翼翼處理內存與資源損耗中解放出來。

本文中我將解釋緣何內存泄露依然存在以及如何避免其出現。別擔心,本文不涉及GC內部工作機制及其它.net的資源及內存管理等高級特性中。

理解泄露本身及如何避免其出現很重要,尤其因爲它無法輕鬆地自動檢測到。單元測試在此方面無能爲力。一旦產品中你的程序崩潰了,你需要馬上找出解決方案。所以在一切都還不是太晚前,花些時間來學習一下本文吧。

Table of Content

·         介紹

·         泄露?資源?指什麼?

·         如何檢測泄露並找到泄露的資源

·         常見內存泄露原因

·         常見內存泄露原因演示

·         如何避免泄露

·         相關工具

·         結論

·         資源

介紹

近期,我參與了一個大的.net項目(暫叫它項目X吧),我在項目中負責追蹤內存與資源泄露。大部分時間我都花在與GUI關聯的泄露上,更準確地說是一個基於Composite UI Application Block (CAB).的windows窗體應用。接下來我要說的直接應用到winform上的內容,多數見解同樣可以適用到其它.net應用中(像WPF,Silverlight,ASP.NET,Windows service,console application 等等)。

我不是個處理泄露方面的專家,所以我不得不深入鑽研了一下應用程序,做一些清理工作。本文的目標是與你們分享在我解決問題過程中的所得所悟。希望能夠幫助那些需要檢測與解決內存、資源泄露問題的朋友。下面的概述部分首先會介紹什麼是泄露,之後會看看如何檢測到泄露和被泄露資源,以及如何解決與避免類似泄露,最後我會列出一個對此過程有幫助的工具列表及相關資源。

泄露?資源?指什麼?

內存泄露

在進一步深入前,讓我們先來定義下我所謂的“內存泄露”。簡單引用在Wikipedia上找到的定義吧。該定義與我打算通過本文所幫助解決的問題完美的一致:

在計算機科學領域中,內存泄露是指一種特定的內存損耗,該損耗是由一個計算機程序未成功釋放不需要的內存引起的。通常是程序中的BUG阻礙了不需要內存的釋放。

仍然來自Wikipedia:”以下語言提供了自動的內存管理,但並不能避免內存泄露。像 Java,C#,VB.Net或是LISP等。”

GC只回收那些不再使用的內存。而使用中的內存無法釋放。在.net中,只要有一個引用指向的對象均不會被GC所釋放。

句柄與資源

內存可不是唯一被視爲資源的。當你的.net應用程序在Windows上運行時,消耗着一個完整的系統資源集。微軟定義了系統三類對象:用戶(user),圖形設備接口(GUI),以及系統內核(kernel)。我不會在此給出完整的分類對象列表,只是指出一些重要的:

·         系統通過使用用戶對象(User objects) 來支持windows管理。相關對象包括:提速緩衝表(Accelerator tables),Carets(補字號?),指針(Cursors),鉤子(Hooks),圖標(Icons),菜單(Menus)和窗體(Windows)。

·         GDI對象 支持圖形繪製:位圖(bitmaps),筆刷(Brushes),設備上下文(DC),字體(Fonts),內存設置上下文(Memory DCs),元文件(Metafiles),畫筆(Pens),區域(Regions)等。

·         內核對象 支持內存管理,進程執行和進程間通訊(IPC):文件,進程,線程,信號(Semaphores),定時器(Timer),訪問記號(Access tokens),套接字(Sockets)等。

所有系統對象的詳細情況都可以在MSDN中找到。

系統對象之外,你還會碰到句柄(handles).據MSDN的陳述,應用程序不能直接訪問對象數據或是對象所代表的系統資源。取而代之,應用程序一定都會獲得一個對象句柄(Handle),可以使用它檢查或是修改系統資源。在.net中無論如何,多數情況下系統資源的使用都是透明的,因爲系統對象與句柄都由.net類直接或間接代表了。

非託管資源

像系統對象(System objects)這樣的資源自身都不是個問題,但本文仍涵蓋了它們,因爲像Windows這樣的操作系統對可同時打開的 套接字、文件等的數量都有限制。所以關注應用程序所使用系統對象的數量非常重要。

在特定時間段內一個進程所能使用的User與GDI對象數目也是有配額的。缺省值是10000個GDI對象和10000個User對象。如果想知道本機的相關設置值,可以使用如下的註冊表鍵:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows: GDIProcessHandleQuota 和 USERProcessHandleQuota.

猜到了什麼?確實沒有這麼簡單,還有一些你會很快達到的其它限制。比如參照:我的一篇有關桌面堆的博客 所述。

假設這些值是可以自定義的,你也許認爲一個解決方案就是打破默認值的限制—調高這些配額。但我認爲這可不是個好主意,有如下原因:

1. 配額存在的原因:系統中不是隻有你獨自一個應用程序,所有運行在計算機中的其它進程與你的應用應該分享系統資源。

2.  如果你修改配額,使它不同於其它系統了。你不得不確認所有你的應用程序需要運行的機器都完成了這樣的修改,而且這樣的修改從系統管理員的角度來說是否會有問題也需要確認。

3.  大部分都採用了默認配額值。如果你發現配置值對你應用程序來說不夠,那你可能確實有些清理工作要做了。

如何檢測泄露及找到泄露的資源

泄露帶來的實際問題在MSDN上的一篇文章中有着很好的描述:

哪怕在小的泄露只要它反覆出現也會拖垮系統。

這與水的泄露異曲同工。一滴水的落下不是什麼大問題。但是一滴一滴如此反覆的泄露也會變爲一個大問題。

像我稍後解釋的,一個無意義的對象可以在內存中維持一整圖的重量級對象。

仍然是同一篇文章,你會瞭解到:

通常三步根除泄露:

1.發現泄露

2.找到被泄露的資源

3.決定在源碼中何時何處釋放該資源

最直接“發現”泄露的方式是遭受泄露引發的問題

你或許沒有見過內存不足。“內存不足”提示信息極少出現。因爲操作系統運行中實際內存(RAM)不足時,它會使用硬盤空間來擴展內存。(稱爲虛擬內存)。

在你的圖形應用程序中可能更多出現的是“句柄不足”的異常。準確的異常不是System.ComponentModel.Win32Exception 就是 System.OutOfMemoryException 均包含如下信息:”創建窗體句柄錯誤”。這兩個異常多發於兩個資源被同時使用的情況下,通常都因爲該釋放的對象沒有被釋放所致。

另外一種你會經常碰到的情況是你的應用程序或是整個系統變更得越來越慢。這種情況的發生是因爲你的系統資源即將耗盡。

我來做個生硬的推斷:大多數應用程序的泄露在多數時間裏都不是個問題,因爲由泄露導致出現的問題只在你的應用程序集中使用很長時間的情況下才會出現。

如果你懷疑有些對象在應該被釋放後仍逗留在內存中,那需要做的第一件事就是找出這些對象都是什麼。

這看起來很明顯,但是找起來卻不是這樣。

建議通過內存工具找到非預期逗留在內存中的高級別對象或是根容器。在項目x中,這些對象可能是類似LayoutView實例一樣的對象們(我們使用了MVP(Model View Presentation )模式)。在你的實際項目中,它可能依賴於你的根對象是什麼。

下一步就是找出它們該消失卻還在的原因。這纔是調試器與工具能真正幫忙的。它們可以顯示出這些對象是如何鏈接在一起的。通過查看那些指向“殭屍對象”(the zombie object)的引用你就可以找到引起問題的根本原因了。

你可以選擇 ninja方式(譯者:間諜方式?)(參照 工具介紹章節中有關 SOS.dll 和 WinDbg 的部分)。

我在項目X中用了JetBrains的dotTrace,本文中我將繼續使用它來介紹。在後面的工具相關章節中我會向你更多的介紹該工具。

你的目標是找到最終引起問題的那個引用。不要停留在你找到的第一個目標上,但是也要問問自己爲什麼這個傢伙還在內存中。

常見內存泄露的原因

上面提到的泄露情況在.net中較常見。好消息是造成這些泄露的原因並不多。這意味着當你嘗試解決一個泄露問題時,不需要在大量可能的原因間搜尋。

我們來回顧一下這些常見的罪魁禍首,我把它們區別開來:

·         靜態引用

·         未註銷的事件綁定

·         未註銷的靜態事件綁定

·         未調用Dispose方法

·         Dispose方法未正常完成

除了上列典型的原因外,還有些其它情況也可能引發泄露:

·         Windows Forms:綁定源濫用

·         CAB:未移除對工作項的調用

我只列出了可能在你應用程序中出現的一些原因,但應該清楚你的應用程序依賴的其它.net代碼、庫實際使用中也可能引發泄露。

我們來舉個例子。在項目x中,使用了一套第三方控件來構造界面。其中一個用來顯示所有工具欄的控件,它管理着一個工具欄列表。這種方式沒什麼,但有一點,即使被管理的工具欄自身實現了IDisposable接口,管理類卻永遠也不會去調用它的Dispose方法。這是一個bug.幸運的是這發生在一個很容易發現的工作區:只能我們自身來調用所有工具樣的Dispose方法了。不幸的是這還不夠,工具欄類自身問題也不少:它並沒有釋放自身承載的控件(按鈕,標籤等等)。所以在解決方案中還要添加對每個工具欄中控件的釋放,但是這次可就沒那麼簡單了,因爲工具欄中的每個子控件都不同。不管怎麼樣這只是一個特殊的例子,我要表達的觀點是你應用程序中使用的任何第三方庫、組件都可能引發泄漏。

最後,還有一種由.net framework造成的泄露,由一些不好的使用習慣引起。即使.net framework自身可能引發泄露,但這是你極少會遭遇到的情況。把責任推到.net身上很容易,但在我們把問題推到別人頭上前,還是應該先從自身寫的代碼出發,看看裏面有沒有問題。

常見泄露演示

我已經列舉出了泄露主要的來源,但我還不想僅限於此。如果每個泄露我都能舉個鮮活的例子的話,我想本文會更實用些。好,我們先啓動Vs 和 dotTrace , 然後看些示例代碼。我會同時演示如何解決或是避免每個泄露情況。

項目X中使用了CAB和MVP模式,這意味着界面由工作空間、視圖和呈現者組成。簡單起見,我決定使用包含一組窗口的Winform應用。其中使用了與Jossef Goldberg的一篇關於“Wpf應用程序內存泄露”文章中相同的方法。甚至我會直接把相同的例子和事件處理函數應用到我的Winform App中。

當一個窗體被關閉及處置後,我們期待的結果是它同時也在內存中被釋放了。對吧?但我下面要展示的是何種情況下該窗體未被釋放。

下面的就是我創建的示例程序中的主窗口:

這個主窗口可以打開不同的子窗口;每個打開的子窗口都會分別引發不同的內存泄露。

本文相關示例代碼在後面的“資源”一節中可以找到。

靜態引用

我們先把顯而易見的放在一邊。如果一個對象被一個靜態字段引用,那它永遠也不會被釋放。

像singletons模式中就是如此。每個Singletone對象通常都是一個靜態對象,即使不是靜態對象,那它至少也是會長期存在的對象。

這種情況很顯而易見,但是記住不只是直接引用才危險。真正的危險往往來自間接引用。事實上,你一定要注意引用鏈。整個引用鏈中有多少個根。如果有靜態對象作爲根,那所有它的子節點將會始終存在着。

上圖上如果Object1是靜態的話,多數情況下會長時間存在,那所有它下面的引用將會一直在內存中保留着。危險就在於鏈條太長了以至於忽略了根節點是靜態的。如果你只關注在一個深度級別上,考慮Object3和Object4一旦Object2離開內存那它們也就被釋放了。這個沒錯,確定是,但你需要考慮到它們可能因爲Object1一直存在而未被釋放。

小心各種靜態類型。如果可能儘量不用。非要使用的話,花些時間關注它們。

一個來自特定類型的風險—靜態事件,我會在講解常規事件相關內容時介紹它。

事件,或 “失效監聽器”問題

一個子窗口訂閱了主窗口中的一個事件,以便主窗口透明度變化時得到通知(包含在EventForm.cs文件中):

C#

mainForm.OpacityChanged += mainForm_OpacityChanged;

問題就是這個針對OpacityChanged事件的訂閱創建了一個從主窗口到子窗口的引用。

下圖顯示了完成事件訂閱後兩個對象間是如何通訊的:

看看我這篇更多學習事件與引用的博文。下圖就是該文章中體現事件觀察與被觀察者背後引用關係的:

下圖所示就是你使用dotTrace搜索EventForm然後點擊“最短路徑”的結果:

應能看到主窗體(MainForm)保留着對事件窗體(EventForm)的引用。這種情況會出現了第一個你在應用中打開的事件窗口(EventForm)。這就意味着所有在應用中打開的事件窗口(EventForm)只要程序還未銷燬,哪怕你已經不在使用它們了(包括關閉了它們)。

這些子窗口不光只是賴在內存中,它們還可能會引發異常,比如你改變了主窗口(MainForm)的透明度(opacity),那些已經被關閉的事件窗口(EventForm)就會引發異常,因爲主窗口依然通過事件通知他們,但它們已經是“已處置(disposed)窗口”了。

最簡單的解決方案就是通過在這些事件窗口(EventForm)被處置(dispose)時取消事件訂閱,從而移除主窗口對事件窗口的引用:

C#

Disposed += delegate { mainForm.OpacityChanged -= mainForm_OpacityChanged; };

注意:我們這裏有一個問題,MainForm對象在整個應用被關閉後依然在內存中存在。較短的生命週期內的對象相互引用可能不會引起內存問題。任何孤立的對象鏈都會被GC自動從內存中卸載掉。孤立的對象鏈由兩個單向引用的對象或是一組沒有外部引用的連接對象組成。

另一個解決方案是使用基於弱引用的弱委託。在我的那篇《事件與引用》的博文中有涉獵。網上也有幾篇文章講解了如何付諸實現。比如這篇:“弱引用事件”。找到的多數解決方案都是基於弱引用類。更多弱引用方面的學習可以參見MSDN

注意一下,一個以“弱事件模式”形成的解決方案已經在WPF中存在了。

現有的一些框架中如:CAB (Composite UI Application Block) 或Prism (Composite Application Library) 均有一些其它的解決方案,像EventBrokerEventAggregator 。只要你想也可以使用自己實現的其它事件模式:broker/aggregator/mediator

訂閱了靜態對象或是生命週期較長的對象上的事件卻沒有適時取消訂閱也會造成問題。另外一種問題來源於靜態事件。

靜態事件

來直接看個例子(StaticEventForm.cs中):

C#

SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;

這次的例子與前面的很相似,不同的是這次我們訂閱的是一個靜態事件。因爲是一個靜態事件,所以對應的監聽對象永遠也不會被釋放掉。

解決方案就是當我們的監聽者完成所要做的事情後取消該靜態事件訂閱。

C#

SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;

Dispose方法沒有被調用的情況

你是否已經開始注意事件和靜態類型了呢?很好,但這還不夠。你仍能發現一些遊弋的未釋放對象,即使你寫了正確的清理代碼。這種情況的發生有時僅僅只是因爲這些清理的代碼未被調用。。。

在Dispose方法或是Disposed事件中取消事件訂閱和釋放資源是很好的習慣,但如果未調用Dispose那也沒有用。

再來看個有趣的例子。下面示例代碼給一個窗體創建了一個上下文菜單(來自ContextMenuStripNotOKForm.cs):

C#

ContextMenuStrip menu = new ContextMenuStrip(); menu.Items.Add("Item 1"); menu.Items.Add("Item 2"); this.ContextMenuStrip = menu;

在窗口已經關閉且Dispose的情況下,下圖是你可以通過dotTrace 看到的結果:

ContextMenuStrip仍在內存中!注意:想復現該問題,先通過右鍵鼠標顯示上下文菜單,然後關閉窗口。

這就是一個由靜態事件引發的泄露。通常解決方法就是下面的在Disposed事件處理程序中調用ContextMenuStrip的Dispose方法。

C#

Disposed += delegate { ContextMenuStrip.Dispose(); };

我猜你已經開始明白如果不多加小心,在.net中使用事件是很危險的。此處我想強調的是隻此一行代碼就輕易的引起了泄露。而當你創建一個Context-menu時是否考慮過潛在的內存泄露風險呢?

可能比你想象的還要糟。不只ContextMenuStrip未被釋放,它使得整個窗體都還在內存中保留着!在下面截圖中你可以看到ContextMenuStrip引用着窗體:

導致的結果就是隻要ContextMenuStrip還在內存中,整個窗體也會被釋放。哦,當然你也不要忘了,因爲窗體還在,與其相關的一組對象都會持續保留在內存中 – 像窗體上的控件包含的組件,如下圖所示:

這是我嚴重警告一定要對這種情況足夠注視的原因。可能因爲一個小對象而使內存中一個大的對象鏈不能被正常釋放。我總能在項目X中看到這種情況的出現。水滴石穿,不要小看一滴水能帶來的破壞。

因爲單個控件不是指定它的父控件就是指向父控件的事件,所以這使得如果某個控件沒有被調用dispose而致整個對象鏈都無法釋放的潛在可能。這當然也包括上級容器包含的其它控件。所以示例中導致整個窗體還問題存在於內存中的情況也就可能出現(至少在整個應用程序完全終止前是這樣的)。

在這個具體例子中,你可能正在考慮是否有ContextMenuStrip的地方總是出現這個問題呢。當然不會問題出現,使用設計器直接在窗體上直接創建,至少在此情形下,Vs自動生成的代碼可以確保ContextMenuStrip相關組件可能被正確處置。

如果你對於設計器是如何處理的好奇,可以看看ContextMenuStripOKForm類及它的字段在ContextMenuStripOKForm.Designer.cs中是如何處理的。

我想指出另外一個在項目x 中看到的解決方法。由於某些原因,有些控件在源文件中沒有與之相關的.Designer.cs文件。設計器代碼生成的代碼就在.cs文件中。別問我爲什麼。除了與衆不同的代碼結構(不推薦)外,問題在於這些代碼是被完整拷貝過去的:但要嗎Dispose方法就沒有,要嗎就是沒有調用組件的Dispose方法。我想你能明白這種情況下出問題就不奇怪了。

不完整的Dispose方法

我想現在你已經領會了調用那些實現Dispose模式類的Dispose方法的重要性了,強調Dispose這是我要說的一件事情。在你的類中實現IDispose接口以及包含其它Dispose的調用這非常棒,對你的程序也非常有益,只要Dispose方法實現正確。

這段評論看起來好像有點兒傻,但是我這麼做是因爲我看到太多這種不完整實現Dispose的例子了。

你清楚它是怎麼發生的。你創建好了自己的類;實現了IDisposable接口;你在Dispose方法中取消事件訂閱和釋放資源;並且你在所有需要調用Dispose的地方都調用了它。這很好,直到後來你的類中有一個訂閱了新的事件或是消耗了新的資源。編碼很容易,你熱情洋溢的完成編碼、測試。它運行起來很好,所以你很高興。你提交代碼,不錯!但是。。。唔,你忘記了更新Dispose方法去釋放新的事件或資源了。這種事情總在發生。

我就不舉例了。這應該相當常見。

Windows窗體:綁定源誤用

我們來解決一個Windows窗體上的問題。如果你用BindingSource組件,請確認你是按照它設計時給定的方式使用的。

通過靜態引用,我已經看到了出現的BindingSource,而BindingSource的運轉方式導致了內存泄露。使用BindingSource作爲數據源的控件都保留着一個BindingSource的引用 ,即使這些控件被處置後。(Disposed)

下圖所示的情況,就是在一個數據源是靜態或長生命週期的BindingSource(如:BindingSourceForm.cs)的ComboBox被處置後 用doTrace查看的結果。

一個解決方案就是用一個BindlingList替代BindingSource.如你可以這樣,拖放一個BindingSource到你的窗體上(指設計時),將BindingList分配給BindingSource作爲它的數據源,同時把BindingSource分配給ComboBox作爲它的數據源。這種方式下,你仍將使用一個BindingSource。

看BindingListForm.cs(例子源碼中)中的這個處理。

這種方式並沒有妨礙你使用BindingSource,但是應該在視圖界面中創建它 (設計時窗體中,從而生成自動化代碼。).總之這樣做是有道理的:BindingSource是一個定義在System.WindowsForms命名空間中的表現層組件。BindingList比較而言,它只是一個集合,不隸屬於可視化的組件。

注意:如果你並不是非用BindingSource不可,可以完全只用BindingList。

CAB:缺少從工作項(WorkItem)上移除

下面是一條對針對CAB應用程序的建議,但是你也可以應用到其它類型的應用中。

工作項(WorkItems)是構建CAB應用程序的中心。一個工作項(WorkItems)就是一個在上下文中保留相關對象軌跡的容器,並且執行依賴注入。通常一個視圖(View)創建後就會增加一個工作項(WorkItems)與之對應。當視圖關閉且被回收後,它應該從對應的工作項(WorkItems)上移除,否則工作項(WorkItems)就會使得它(視圖)始終存於內存中,因爲工作項(WorkItems)中維護着一個指向視圖的引用。

如果你忘記了從對應工作項(WorkItems)上移除視圖,那泄露由此產生。

在項目x中,我們使用了MVP設計模式(Model-View-Presenter)。下圖顯示了一個視圖顯示後不同元素間是如何連接的:

(譯者:此處的插圖有誤,所以未添加)

注意WorkItem通過依賴注入得到presenter。而WorkItem多數時候又會順便把presenter注入到視圖(View)中。爲了確保項目X中的所有內容都能適當的釋放掉,我們使用了下圖所示的一個職責鏈:

當一個視圖(View)被處置(disposed)了(很可能就是因爲它被關閉了),那它的Dispose方法就會被調用。該方法會依次調用presenter的dispose方法。Presenter認識WorkItem,在它的Dispose方法中會把自己和源頭的視圖從Worktem中移除掉。通過這種方式,所有內容都被適當的處置和釋放了。

我們的應用程序框架中包括了實現了上面職責鏈條的基類,所以視圖開發人員不需要重要實現這些類也不用每次都爲此擔心。即使不是CAB應用程序,我也鼓勵在你的應用中實現這類模式。在你的對象中正確實現自動釋放模式將助你避免那些由於疏忽引起的泄露。但要確保所有實現的方式一致,不要因爲他們不知道適當的處理方式而出現每個開發人員實現都不同的情況,這也會導致泄露。

如何避免泄露

現在你對泄露本身以及它是如何產生的有了進一步的瞭解,此時我想強調幾個重點並給出幾個技巧。

我們先來探討一條一般的規則。通常一個負責創建另外一個對象的對象也負責處置(disposing)它。當然不包括工廠類。

反過來:一個對象對於從別的對象上得到的對象沒有處置(disposing)的職責。

事實上,這確實要依靠具體情形而定。無論如何,重要的是謹記對象屬於誰(who created it)。

第二條原則:每一個+=(事件訂閱)都是一個潛在的敵人!

根據我的個人經驗,事件是.net中的主要泄露來源。它值得你反覆確認甚至確認再三。每次你在代碼中增加事件訂閱,都應該考慮一下結果問問自己是否需要再增加一個-=來取消事件訂閱。如果答案是需要,在你沒忘記前馬上加上。經常是加到Dispose方法中。

爲了確保對象被成功回收,推薦的做法是有事件訂閱的對象就需要對應的事件取消訂閱。無論如何,當你絕對清楚一個事件源將不再發布事件通知了,而且你希望所有訂閱它事件的其它對象能被釋放,那麼你可以強制移除所有該事件的訂閱。我一篇博文中包含了如何做的代碼事例。

馬上給出一個技巧。通常當對象引用在一定數量的多個對象間共享時,問題就會出現了。因爲這種情況下明瞭哪個對象引用了哪些引用就很困難了。有時在內存中克隆所需的對象要勝於引用現有對象,這樣可以避免對象間反覆纏繞。

最後,即使這已是.net中衆所周知了,我還是想要再次強調調用Dispose的重要性。每次你分配了一個資源,一定要確保調用了Dispose或是將資源使用的代碼寫在一個using代碼塊中。如果你不是始終都這麼做的話,你很快會被資源泄露搞死,通常情況下都是非託管資源引起的。

Tools

相關工具

幾款工具可能助你追蹤對象實例、也包括系統對象和句柄。我來列舉幾個。

Bear

Bear是一個可顯示出所有Windows下運行進程信息的免費程序:

·         支持所有GDI對象的用法(hDC, hRegion, hBitmap, hPalette, hFont, hBrush)

·         支持所有用戶對象的使用(hWnd,hMenu,hCursor,SetWindowsHookEx,SetTimer 和其它形式的對象)

·         句柄統計

GDIUsage

另外一個實用的工具是GDIUsage.這款工具也是免費的而且開源。

GDIUsage聚集在GDI對象上。通過它,你可以對當前GDI消耗情況拍攝快照,執行一個可能誘發泄露的行爲,之後比較泄露前後資源的使用情況。這樣可以大大的幫助我們,它能讓我們看到操作期間增加了(或是釋放了)哪些GDI對象。

此外,GDIUsage不光只是給出一個數字,還可以提供GDI對象的圖形化顯示。肉眼觀察位圖(bitmap)泄露的內容可以輕鬆的找出泄露的原因。

dotTrace

JetBrains出品的dotTrace是一個.net程序的內存與性能分析工具。

下圖就是dotTrace的截屏。這也是項目X中我用的最多的工具。其它.net分析工具我也不太瞭解,但是dotTrace爲我解決項目X中檢測到的泄露提供了所需的信息。多達20個以上。。。我沒說過這就是一個bug項目?

dotTrace允許你及時標識出特定時間下內存中的對象,它們如何存在(被哪些對象引用),以及它們是誰(誰被引用)。你還可以使用它提供的高級調試功能:追蹤棧分配情況,查看銷燬對象列表等。

下圖展示的是兩種內存狀態間的差別

dotTrace也是一個性能分析工具:

使用的方法就是先啓動dotTrace然後指定exe文件的路徑,之後就可以請求它對你選擇的應用程序進行分析了。

如果你想檢查應用程序的內存使用情況,可以在程序運行時用dotTrace拍攝快照,然後讓它顯示出相應信息。你要做的第一件事大概就是讓它顯示出指定類在內存中有多少個實例,以及這些實例是如何存在的。

除了搜索託管實例外,你也可以搜索非託管資源。dotTrace沒有對非託管資源追蹤提供直接支持,但你可能搜索對應的.net包裝對象。例如:你搜索位圖、字體或是筆刷類的實例。如果你發現一個實例沒有被釋放,那麼它在應用程序上分配的資源也仍然還在。

下一個我要介紹的工具就內置了對非託管資源的追蹤。也就是說通過它你就能夠直接探索HBITMAP, HFONT 或是 HBRUSH 句柄。

.net內存分析器

.net內存分析器是另一個有趣的工具。它提供了一些dotTrace不包括的實用特性:

·         查看哪些已經調用了處置方法,卻還存在的對象

·         查看哪些已經被釋放卻沒有調用處置方法的對象

·         非託管資源的追蹤

·         附加到一個運行中的進程上

·         附加到一個進程的同時作爲VS的調試器

·         自動內存分析(關於常見內存使用問題的提示和警告)

另外一些可用的內存分析器

上述幾個工具只是一些工具所能幫助你的示例。dotTrace和.NET Memory Profiler是衆多.net內存及性能分析工具中的兩個。其它一些有名的包括:ANTS Profiler,YourKit Profiler,PurifyPlusAQtime 和 CLR Profiler.這些工具中的多數都提供了和dotTrace相同類型的功能。在SharpToolbox.com上你可以找到完整的專注於.net的分析工具集合

SOS.dll and WinDbg

另一個你可用的工具是SOS.dll. SOS.dll 是一個擴展調試的工具,幫助你在WinDbg.exe調試器和VS中調試託管程序,提供CLR資源有關的內部信息。可以用它來獲取與GC相關的信息,與內存中對象、線程與鎖、調用棧等相關的信息。

WinDbg 是你通常需要附加到產品中某個進程時用的較多的工具。關於SOS.dll 和 WinDBg 如果想更多瞭解可以看看 Rico Marian 的一篇博文,和Mike Taulty的兩篇博文(SOS.dll 與 WinDbg 和 SOS.dll 與 Visual Studio) ,還可以參照Wikipedia

SOS.dll 和 WinDbg 作爲Windows調試工具包中的一部分由微軟免費提供。二者比之上述的其它工具的一大優勢就是在保持強大功能的同時又有着較低的資源損耗。

下圖是使用sos.dll和gcroot命令的示例輸出:

WinDbg screenshot:

自定義工具

除去市面上的可用工具外,別忘了你也可以創建自己的工具。可以在多個應用程序中重用的獨立工具。當然開發這種工具可能有點兒難度。

我們爲項目X開發了一套完整工具,幫助我們保持實時的資源使用情況和潛在泄露情況進行跟蹤。

這些工具之一在右側顯示一組存在與消亡的對象列表。它由一個CAB服務和一個CAB視圖組成,可以用來檢查我們期望被釋放的對象是否真的被釋放了。

下圖就是該工具的截圖:

如果保留應用中所有對象的痕跡,對於大的應用程序來說代價太高並且也違背產品設定的對象數量。事實上,我們無需關注所有的對象,只要關注那些應用中的高級別對象和根容器。這些對象我在解釋如何檢測泄露時已經提議過進行追蹤。

創建該 工具使用的技術很簡單。它使用了弱引用。弱引用類允許你引用一個同時可被GC回收的對象。此外,它還通過提供IsAlive屬性提供你測試該引用是否已消亡的功能。

項目X中,我們還有一個提供GDI和用戶對象使用概況的小部件。

當資源接近枯竭之時,這個小部件會有一個小的警告圖標:

此外,該工具還會讓用戶關閉一些當前打開的窗口/選項卡/文檔,並且阻止用戶打開新的窗口等,直到資源使用情況重新低於臨界級別。

爲了讀取到當前UI資源使用情況,我們使用了User32.dll中的GetGuiResourcesAPI。下面代碼展示瞭如何在C#中引入該API:

C#

// uiFlags: 0 - Count of GDI objects

// uiFlags: 1 - Count of USER objects

// GDI objects: pens, brushes, fonts, palettes, regions, device contexts, bitmaps, etc.

// USER objects: accelerator tables, cursors, icons, menus, windows, etc.

[DllImport("User32")]

extern public static int GetGuiResources(IntPtr hProcess, int uiFlags);

public static int GetGuiResourcesGDICount(Process process)

{

  return GetGuiResources(process.Handle, 0);

}

public static int GetGuiResourcesUserCount(Process process)

{

  return GetGuiResources(process.Handle, 1);

}

通過Process.GetCurrentProcess()的WorkingSet64屬性,獲取內存使用情況。

結論

我希望本文爲你改善應用程序和解決泄露方面提供了一個良好的基礎。追蹤泄露可以很有趣。。。如果你確實沒什麼其它更好的事情做的話:-)有時,你別無選擇,因爲對於你的應用程序來說解決泄露至關重要。

一旦解決了泄露,仍有工作要做。我強烈建議你在儘量降低資源消耗方面改善應用程序。不要損失功能。最後,我邀請你閱讀我的另外一篇包含相關建議的博文

相關資源

演示程序源代碼下載地址

假如你有意進一步鑽研,下面是一些有趣的補充資源

·         Jossef Goldberg: Finding memory leaks in WPF applications

·         Tess Ferrandez has a series of posts about memory issues (ASP.NET, WinDbg, and more)

·         MSDN article by Christophe Nasarre: Resource Leaks: Detecting, Locating, and Repairing Your Leaky GDI Code

·         Article in French by Sami Jaber: Audit et analyse de fuites mémoire

·         My blog post about the Desktop Heap

·         My blog post about lapsed listeners

·         My blog post that shows how to force unsubscription from an event

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章