記憶體不足(OutOfMemory)的除錯分析

查志強發表於2015-01-08

【原文:http://blog.csdn.net/lazyleland/article/details/6704661

32位作業系統的定址空間是4G,其中有2G被作業系統佔用,也就是說留給使用者程式的記憶體只有2G(其中還要扣除程式載入時映像佔用的部分空間,一般只有1.6G~1.8G左右可以使用)。

如果程式執行中需要申請記憶體,而作業系統無法為其分配記憶體空間,則會產生記憶體不足的異常,在.net中為System.OutOfMemoryException(The exception that is thrown when there is not enough memory tocontinue the execution of a program.)。

雖然最終的表現都為OutOfMemoryException,但其產生的原因可能是不一樣的,動手解決此問題之前需要先對程式當前記憶體的使用狀態進行分析,找出正確的原因,才能對症下藥。下面分享一下除錯此類問題的一些心得。

一、使用Perfmon.exe

1)   命令列輸入perfmon.exe。開啟“效能”。

2)   在“效能日誌與警報-計數器日誌”上右鍵,選擇“新建日誌設定”。

3)   輸入日誌名稱,如“OOM”。

4)   在“常規-計數器”中刪除所有預設的計數器(如果有)。

5)   點選“新增計數器”,效能物件選擇“.NET CLR Memory”,計數器選擇並新增“Bytes in all heaps”、“Large Object Heap Size”。同樣“效能物件”選擇“Process”,計數器選擇並新增“Virtual bytes”、”Private bytes”。注意點選“新增”前需要在“從列表選擇範例”選擇框選擇需要監控的程式。


另外,如果當前系統登陸的用例對目標程式沒有除錯許可權,需要在“執行方式”框裡填入domain\username,並輸入密碼。

6)   資料取樣間隔可以設定小一點,如1秒鐘。

7)   點選“確定“,新的計數器日誌就新建成功了。右邊的框框中可以看到新的計數器,綠色表示正在執行中。”“日誌檔名“列顯示了本次監控結果將寫入的日誌檔名(同一個計數器執行多次,寫入的日誌檔名是不同的)。


8)   讓程式與計數器執行一段時間,然後停止計數器(為什麼要停止計數器?我的機器上測試的時候,需要先停止計數器後,才會把監控的結果寫到日誌檔案中,如果不先停止,在下面的監視器中將看不到計數器執行這段時間的監控結果。)。

9)   點選“系統監視器“。點選”“檢視日誌資料”(圖示為)按鈕,在“來源”選項卡里新增日誌檔案為剛剛我們新建的計數器產生的日誌檔案。下方可選擇時間範圍,這裡選全部即可。然後在“資料”選項卡里新增需要檢視的計數器(此選擇卡還可以定義不同的計數器顯示的樣式及顯示比例)。



10) 從圖上可以看到在計數器執行的時間段中,被監控程式的記憶體使用情況。在新增計數器的視窗中有對相應計數器的簡單說明,下面是幾個常用的計數器:

·           Bytes in all Heaps:.net託管堆(GC)使用的總記憶體。包括0代、1代、2代及大物件堆。

·           Large Object Heap size:大物件堆使用的記憶體。.net在分配記憶體時大於85K的物件會被放到這個堆中,不同於0、1、2代,大物件堆中的記憶體不是連續的,在垃圾回收時也不會移動大物件的地址(我係統顯示為大於20K物件為大物件,實際上2.0應該為大於85K)。

·           Private bytes:該計數器記錄了當前通過VirtualAlloc API Commit的Memory數量。無論是直接呼叫API申請的記憶體,被Heap Manager申請的記憶體,或者是CLR 的managed heap,都算在裡面。跟Handle Count一樣,如果在整個程式週期內總體趨勢是連續向上,說明有MemoryLeak(摘自百度)。

·           Virtual bytes:該計數器記錄了當前程式申請成功的使用者態總記憶體地址,包括DLL/EXE佔用的地址和通過VirtualAlloc API Reserve的Memory Space數量,所以該計數器應該總大於Private Bytes。一般來說,Virtual Bytes跟Private Bytes的變化大致一致。由於記憶體分片的存在, Virtual Bytes跟Private Byes一般保持一個相對穩定的比例關係。當Virtual Bytes跟Private Bytes的比例關係大於2的時候,程式往往有比較嚴重的記憶體地址分片(摘自百度,但對.net程式來說一般差別在200M以下還算是正常的)。

11) 有了上面幾個計數器的結果之後,一般可以通過以下規則大致定位問題的所在:

·           Virtual bytes增長但Private bytes沒有顯著增長。為Virtual bytes洩露。

·           Private bytes增長但bytes in all heaps沒有顯著增長。為非託管資源洩露,檢查有沒有COM元件或其它非託管呼叫沒有正確釋放記憶體。

·           Bytes in all heaps顯著增長。為.net託管記憶體洩露。由於.net記憶體是GC管理的,自動回收,這裡有可能是快取了過多的資料,或程式中引用混亂導致本來需要被回收的資料還被其它物件所引用從而GC沒法回收這部分資料。

·           Bytes in all heaps有增長但使用不多,系統剩餘可用記憶體也比較多(需要再新增相應的計數器)。這種情況比較少見,但我遇到過一次是由於非託管在存在大量碎片,導致.net在申請大物件時失敗。

二、使用Windbg

如果是由於.net託管記憶體導致的記憶體洩露,可以用Windbg進一步排查問題(非託管的也可以,但還沒有對這方面進行詳細研究過:))。

1)   載入SOS.dll。

.loadC:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll

2)   儲存程式的映像檔案。

.dump /ma “c:\oom.dmp”

3)   檢視記憶體的使用情況。

!address –summary


RegionUsageIsVAD:VirtualAlloc的記憶體,一般為GC佔用。

RegionUsageFree:可用記憶體。

RegionUsageImage:載入dll或exe佔用的記憶體。

RegionUsageStack:執行緒堆疊佔用的記憶體(.net中如果一個遞迴函式有問題導致無限迴圈呼叫會產生StackOverflowException)。

其它的可以參考Windbg文件,或打!address -?獲得命令說明。另外上面有一個重要的資訊,即Largest free gegion,我這裡關心其size為18280KB,即是說當前可申請的最大連續記憶體塊為18M多,也意味著如果此時程式去申請大於此數值的記憶體,也會報OutOfMemory(儘管目前Free的記憶體總共還有400多M,打!address –RegionUsageFree可以看到這400多M的記憶體的分塊情況),通常引起此問題的原因,可能是非託管呼叫引起的嚴重記憶體碎片,因為託管的記憶體是連續的。由於大物件申請失敗的問題除錯,後面還會再進一步詳細說明。

4)   檢視託管堆記憶體的使用情況。

!eeheap –gc


上面顯示了GC各個代及大物件堆的大小及每個段(segment)的大小、地址範圍等等資訊。GC在分配記憶體的時候是按段申請,按段釋放的,也就意味著,GC佔用的記憶體要比你的程式中為物件實際申請的總記憶體要大一點,如果程式為物件申請一塊記憶體,而當前段的最大可用記憶體不足以分配時,GC為向系統申請新的段,從上面看到段的大小為16M左右,應該是按某種演算法得出新段的大小(比如當前可用記憶體,作業系統或.net framework的版本等,只是我的猜測,有興趣的童鞋自己查查文件後告訴我:))。

5)   檢視當前託管堆中的物件,及每種物件佔用的記憶體大小。

!dumpheap –stat



從上圖中看到最大的型別為字串,共佔用了135M記憶體。

6)   檢視某種型別的所有例項地址。

!dumpheap -mt 793308ec


7)   檢視某個物件的資訊。

!do [物件地址]

物件地址可以在!dumpheap –mt命令的第一列中得到。

8)   檢視某個物件佔用的記憶體大小。

!objsize [物件地址]

如果物件引用了其它物件,此命令會把其引用的其它物件佔用的記憶體也算進去。

9)   檢視陣列中的元素。

!dumparray [陣列物件地址]

如果用!do得到的物件為陣列,用此命令得到陣列中每個元素的地址,再用!do打出陣列元素的資訊。

10) 檢視物件與其它物件的引用情況。

!gcroot [物件地址]

這個命令在判斷.net託管記憶體洩露很有用,它可以得到某個物件沒有被GC釋放掉的原因(因為存在根物件的引用關係)。

11) 檢視物件大小大於某個數值的所有物件。

!dumpheap –min 10000000

12) 檢視大物件堆的物件。

!dumpheap –startatlowerbound [大物件堆的起始地址]

大物件堆的起始地址可以由命令!eeheap –gc得到。

13) 除錯由大物件記憶體分配不足引起的OutOfMemory。

有些情況下,明明記憶體還剩下很多,但是由於非託管帶來的記憶體碎片,導致連線記憶體不足以分配程式申請的大物件的記憶體,這時也會報記憶體不足的異常。要確定記憶體不足是否由此原因引起的可按以下步驟除錯:

在程式申請大物件的時候,用windbg打一個斷點,並把大物件申請的記憶體的大小列印出來。

0:027>x mscorwks!WKS*allocate_large*

79f7d9ebmscorwks!WKS::gc_heap::allocate_large_object = <no type information>

0:027>bp 79f7d9eb "?@ebx;!clrstack"

如果程式申請大物件,會有類似下面的輸出,大物件的大小為52M。

Evaluateexpression: 52679596 = 0323d3ac

此時可以在輸出裡看到堆疊,確定是程式哪個程式碼需要申請這麼大的物件,是否屬於正常。也可以用!address –summary看當前可申請的最大連線記憶體塊大小,如果小於待申請的大物件大小,則會出現記憶體不足。

另外,在以前的除錯中我得到這麼一個結論,如果程式宣告瞭一個長度大於85000/4=21000的陣列,這時陣列實際佔用的記憶體大於85K,GC會把這個陣列放在大物件堆中,對於List型別,其長度是可以動態增加的,如果長度從小於21000到達到21000,GC也會把它移到到大物件中(剛一開始長度小於21000時不在大物件堆中)。

14) 檢視GC的終結佇列及執行緒。

另一種導致託管記憶體沒有被釋放的原因(除了物件被引用)就是GC的終結執行緒被阻塞了,從而導致可以釋放的物件來不及被釋放。可以按以下步驟除錯此類問題:

!finalizequeue

有類似以下的輸出:

這裡需要關注的是Ready for finalization XX objexts,表示終結佇列中有多少個物件正在等待被回收,如果數量比較大,可以進一步看看終結執行緒的堆疊。

!threads

後面帶有Finalizer的即終結執行緒。

~[執行緒號]s

!clrstack

根據堆疊資訊,可以初步斷定引起終結執行緒阻塞的程式碼位置,然後針對程式碼做進一步的分析。

三、結語

本文只是列出了除錯問題的一些方法或工具,在除錯過程中還是要靈活處理,耐心分析,必要時還要查閱文件或研究程式碼,畢竟導致問題的原因是多種多樣的,很難有一個標準的除錯過程。另一個重要的就是在開發過程中多注意細節,瞭解開發語言的特性,能夠把問題消滅在開發過程中,那就再好不過了。


轉載請註明原文地址:)


相關文章