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
根據堆棧信息,可以初步斷定引起終結線程阻塞的代碼位置,然后針對代碼做進一步的分析。
轉自:http://blog.csdn.net/lazyleland/article/details/6704661