程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET托管內存類應用的內存洩漏分析和診斷

.NET托管內存類應用的內存洩漏分析和診斷

編輯:關於.NET

在托管內存管理中,“洩漏”意義不同與傳統 Native 應用中的忘記顯式釋放(delete/delete[] 等)不同,當然對於非托管資源之類(如句柄等)還是需要在 Finalize (析構方法等同於 Finalize)方法中顯式釋放的,在托管內存管理中“洩漏”對象實例指的是,由於與 Root 對象集中的對象存在本應斷開的引用關系,而讓 GC 線程認為該對象還被使用,因而不能被釋放,盡管其不再會被使用。決大部分情況下,由於應用(程序員)認為該對象不會存在了,而在再次使用時,又在托管堆中再次創建了該對象實例,可以想象這樣的後果很嚴重,隨著創建次數增加堆內存會爆滿。(托管堆中 G3 區爆滿,G2 區無法騰出空間)。

GC 判斷一個對象是否可以被釋放是通過從被稱為 Root 對象集中的根對象開始(如 Main 函數的 args 形參、static 變量及其對象成員等),遍歷出所有被其引用的對象和子對象。GC 執行時通過標記這些引用中的對象,清除未標記上的對象來完成內存釋放(標記、清理算法),當然清除也可能分步(如移送 Finalize 隊列等)。由於標記、清理算法的中斷時間等性能考慮,托管堆會分區(代),當前 CLR 是 3 代 – G1、G2、G3。伴隨 Age(GC 一次 Age 加 1)增加,對象會逐漸從 G1 移送到 G3 代中(復制、整理算法),即 G1 是新生代,都是些短期對象,G3 是老年對象的永久居留地。需要說明的是,實際上在當前版本的 .NET CLR 中有 2 個托管堆(SOH 和 LOH),其中一個叫大對象托管堆(LOH),專門用來存放大於 84, 999 Bytes 的對象。程序只能在 SOH G1 和 LOH 中分配對象空間,只有 CLR GC 線程可以在 SOH 的 G2、G3 中分配(移送)對象。

明白上面的基本道理,下面看看和托管對象實例內存洩漏的圖例:

上面圖中表示的意思是使用一段時間後,堆中對象與 Root 對象的引用關系,其中顏色由淺到深表示了 Age 的因素。如果此時,GC 線程執行,堆情況將如下所示:

其中所有 Unreachable 的對象實例都將被 GC 所釋放,這樣托管堆內存會被正確回收。但需要說明的是,如果在 Reachable 的區域中(這部分 GC 是不會釋放的),有一些被引用的對象在以後不會再使用,而且應用(程序員)在下次使用時還會創建新的對象時“洩漏”就發生了。當涉及對此類對象創建操作的業務被用戶反復執行後,CLR 的 G3 代托管堆段會逐漸增長,服務的死期也就不遠了。

有了以上的知識,可以說對內存洩漏的結構化診斷、定位方法如下:

1.監控托管堆使用量(查看進程的內存占用量也可以),找到內存只長不降的業務,這些代碼有內存洩漏的危險。這個過程我一般會使用 LoadRunner 腳本來做,畢竟小尺寸對象的洩漏需要較長的時間才能發生,靠手工操作不靠譜。這個一般不需要並發;

2.重新啟動應用,讓托管堆清理無關對象;

3.執行一次第1步發現的存在內存洩漏缺陷的業務;

4.使用工具將托管堆導出(dump)來,或對托管堆做一次快照(snapshot)。在 dump/ snapshot 前要做一次全面 GC(full GC),盡量把可對象釋放干淨,排除干擾。此時洩漏的對象已經不能 GC 掉了,會保存在托管堆中,都會被 dump/shot 出來;

5重復步驟 3。這會再次創建上次執行時(步驟 3)洩漏的對象;

6步驟 4。此時,洩漏的對象是作為本次 dump/shot 的新對象存在的,相對於步驟 4 中洩漏的同類對象而言;

7對比步驟 4 和 6 兩次 dump/snapshot 結果,下面就需要在茫茫對象中找出洩漏的對象/對象類型來了。實際上這個過程是相對比較困難的,需要了解應用設計,相關背景知識,了然的越詳細,定位越快,結果越准、完整。做兩次 dump/snapshot 的目的在於,洩漏對象將屬於“新”創建對象集范圍,這將有效縮小需檢查的對象范圍。需要說明的是,這裡的“新”指的是第二個 dump/snapshot 相對於第一個 dump/snapshot 裡存在的新對象;

8前步驟的對象范圍還是比較大,接下來可以從對象類型角度排個檢查的優先級順序:

◆檢查應用命名空間中類型的對象;

◆檢查框架所提供的類型的對象;檢查已經執行過手動關閉/釋放方法的對象。如 .NET 中的 IDisposable 接口的 Dispose 方法。因為一旦調用了這種方法,對象本應該被 GC 所釋放的,它是不應該再存在的。對於此類對象,存在的原因有可能:

A.被短生命周期的對象所引用,如局部變量(包括形參變量)等,造成無法 GC。但在,在再次執行 dump/snapshot 時,它應該已經 GC 釋放掉。

B.應用中設計了“池”,對象雖然被關閉,但它仍然會在池中存放,供下次使用時再打開。一般這種情況比較少見,尤其在 .NET 中,放入池中的對象很少會調用其 Dispose 方法。這方面 Java 也類似。

實際上面所說的結構化方法只是表達個意思,真實的過程會是一個逐步定位、迭代的過程,在理解其中意義的前提下,靈活使用。通過上面的一系列分析、診斷方法來定位到洩漏的對象後,就查找是它們是被哪些對象所引用,即 Root Path 中都有哪些對象,通過修改代碼來切斷不應存在的引用,就可以使洩漏對象進行正常的 Unreachable 狀態,GC 線程也就會正確處理它們了。實際上,重點還是在分析、診斷、定位,修復方法還是很容易找到的。

首先要看下 .NET Memory Profiler 是什麼,就不翻譯了。我理解它實際就是個托管堆的 snapshot 工具,可以標記出非托管資源。顯示實時堆使用圖形的功能實在沒大用。

.NET Memory Profiler is a tool for the .NET Common Language Runtime that allows the user to retrieve information about all instance allocations performed on the garbage collected heap (GC heap) and all instances that reside on the GC heap. The retrieved information is presented in real time, both numerically and graphically.

再說說這個ASP.NET 應用,前兩天事業部的一個 ASP.NET 應用出現了內存洩漏的情況。現象是,在一個查詢業務場景中,發現查詢幾次之後 IIS 的 w3p 工作進程會增長幾兆、十幾兆、幾十兆不等的內存(這和查詢結果大小成正比),而且通過 perfmon 監控可以看到 w3p 的 CLR 及時執行了 GC,但托管堆使用量始終只增不減,直到服務宕掉(在 LoadRunner 測試時還有 w3p crash 的情況)。

這裡就不詳細說明使用 .NET Memory Profiler 工具使用細節了,相信能看到這裡的朋友,對這個工具的基本使用肯定不會成問題的。下面直接說重點。

按照上面提供的方法(內存洩漏的結構化診斷、定位方法),首先我們已經找到了在在問題的業務,也能夠重現它。接下來將服務重啟,讓 w3p 的托管堆初始干淨,排除無關對象。然後執行一次有問題的業務,這裡我使用的 LoadRunner Vuser 回放測試腳本。接下來就要請 .NET Memory Profiler 出馬,對 w3p 進程托管堆做次 snapshot。每個快照之前 .NET Memory Profiler 會自動做一次 Full GC。再來做一次執行查詢業務,然後再做snapshot。通過過兩次比較可以看到如下內容:

從中可以了解到:

1.的所有內容是其於 3# snapshot 與 2# snapshot 兩次快照的對比結果;

2..NET Memory Profiler 提供了 Types 所有照到的對象類型,Show types 可以指定顯示類型的范圍,包括所有、“新”對象(第二個 dump/snapshot 相對於第一個 dump/snapshot 裡存在的新對象)等;Type details 每個類型的對象實例信息;Instance details 每個實例的詳細信息;Call stacks/Methods 方法調用棧;Native memory 非托管內存信息;

3.另外需要注意的是 Field Sets,包括 Standard、Dispose Info、Heap Utilization,用於過濾顯示的 Types 類型結果。比較有用,能夠看到上面方法中提到的 Dispose 方法調用後仍然在在的對象類型有哪些,這可以縮小檢查范圍,下面會涉及到。

接下來就需要有針對性的逐一排查了,道先要考慮的是哪種類型的對象最有可能發生洩漏。上面的方法中提供了優先級別。我在這裡是這樣考慮的,因為對應用比較了解,我知道在有問題的業務頁面中,有一個 static 類型的成員變量,它是 XmlDataSource 類型的對象,由於 static 所以實例不會被 GC,所以檢查它所引用的有哪些對象實例就很有意義了。查看該類型的詳細信息可見:

唯一的 XmlDataSource 類型實例是 #13, 483 號(全局實例編號)對象,通過詳細信息可以看到如下被其引用的對象集:

通過對引用它的對象一一檢查後發現,#13, 483 號的 XmlDataSource 類型實例共引用了 6 個 XmlDataSourceView 對象(之所以是 6 個,是因為我做了 3 次 snapshot,每次 LoadRunner 測試腳本中都執行了 2 次查詢操作),通過它們的 Age 可以看到都經過了多次 GC,但都沒有釋放掉。

具體到 Root 對象集的引用路徑也很簡單,只有一條,它們都引用了#13, 483 號實例的事件。通過這樣的分析,我們就能夠找到由於與 static XmlDataSource 類型對象的引用關系,這造成了每次查詢生成的 XmlDataSourceView類型對象都無法被 GC 回收,因此形成了內存洩漏情況。解決辦法也很簡單,可以通過將頁面中 XmlDataSource 類型對象做為非 static 成員變量即可。

通過修復上面這個對象洩漏問題,我們每次業務可以節約接近 11M 的托管內存。但是通過之前的監控來看,每次業務所增加的內存量要比這大很多,而且通過修復上面的問題也不能完全解決內存持續上漲的情況。可以認定,該業務中還存在有內存洩漏的其它位置,需要繼續分析、診斷。這次我通過 Dispose Info 著手,上面說過了,.NET 對象在經過程序調用 Dispose 方法後,就應該不再被使用並被 GC 所回收,如果在 snapshot 發現執行 Dispose 方法後 GC 多次仍未能回收的,就需要關注了。下面就是 Dispose 過後的對象類型:

果然,可以看到在我們業務頁面及其 Master 頁面對象全部都是執行過 Dispose,但未 GC 掉。通過打開類型詳細信息來查看可以發現,他們都被 SiteMap 所引用。

我們都知道 SiteMap 及 XmpSiteMapProvider 等 ASP.NET 2.0 中站點地圖的組件,它們都是由 .NET Framework 提供和管理的,我們是通過 site 文件配置出來的,那麼造成這個原因是什麼呢?通過調查了解到 SiteMap 對象的 SiteMapResolve 事件是static的,而 SiteMap 也是全局性質的,只要我們這些頁面訂閱了 SiteMapResolve 事件,就都會引用到 SiteMap 上去。有興趣的朋友可參見MSDN SiteMap::SiteMapResolve Event的SiteMapResolve causing memory leaks 小節。而且,從對象樹的內存使用量上看也比較合理了,每個頁面由於包括了大量的控件及其狀態數據,因此都很大,每個頁面對象有 46M 多。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved