追蹤內(nèi)存泄漏
【5/8/2005 9:47:52】 【Rico Mariani】 【TechTarget】
如果你認為你的內(nèi)存發(fā)生了泄漏現(xiàn)象,或你正在思索到底是什么東西占用了堆棧,你可以跟著我的步驟來解決困擾你和你的朋友們許久的問題。這個過程很簡單。
這些步驟將會讓你看到如何從發(fā)現(xiàn)一個可疑的內(nèi)存泄漏現(xiàn)象到得出某個特殊的對象引用的過程,這個引用正維持著該對象處于存活狀態(tài)。最后使用這些工具所得到的地址來查看資源的使用情況。
步驟1:運行進程并將你所好奇的進程的狀態(tài)浮現(xiàn)在你腦中確保選擇一個能在你頭腦中重現(xiàn)的情景,否則將不知道是否能在清理內(nèi)存泄漏工作中取得進展。
步驟2:使用tasklist命令來查找進程號
C:\>tasklist
Image Name PID Session Name Session# Mem Usage
========================= ====== ================ ======== ============
System Idle Process 0 RDP-Tcp#9 0 16 K
System 4 RDP-Tcp#9 0 112 K
smss.exe 624 RDP-Tcp#9 0 252 K
...etc...
ShowFormComplex.exe 4496 RDP-Tcp#9 0 20,708 K
tasklist.exe 3636 RDP-Tcp#9 0 4,180 K
大家可以看到我的進程號#4496
步驟3:使用VADump命令來得到進程的摘要
C:\>vadump -sop 4496
Category Total Private Shareable Shared
Pages KBytes KBytes KBytes KBytes
Page Table Pages 35 140 140 0 0
Other System 15 60 60 0 0
Code/StaticData 4596 18384 4020 3376 10988
Heap 215 860 860 0 0
Stack 30 120 120 0 0
Teb 4 16 16 0 0
Mapped Data 129 516 0 24 492
Other Data 157 628 624 4 0
Total Modules 4596 18384 4020 3376 10988
Total Dynamic Data 535 2140 1620 28 492
Total System 50 200 200 0 0
Grand Total Working Set 5181 20724 5840 3404 11480
大家可以看到最大代碼量的進程(18384k)
絕大多數(shù)CLR所使用的資源都處于”Other Data”下----這是因為GC Heap直接被VirtualAlloc進行分配 -- Other Data不會經(jīng)過規(guī)則的窗口堆棧。同樣這種所謂的”loader heaps”也不經(jīng)過規(guī)則的窗口堆棧,loader heaps存放類型信息和被jit處理的代碼。大部分傳統(tǒng)的”Heap”分配都是從正運行的任何未被管理的進程中得到的。這就是帶有控制管道的winform應(yīng)用程序,因此就存在與這些東西相關(guān)的存儲塊。
這兒并沒有太多的”Other Data”,因此堆棧狀態(tài)還是比較正常的。讓我繼續(xù)查看詳細的CLR內(nèi)存使用情況。
步驟4:使用windbg來裝載SOS
C:\> windbg -p 4496
調(diào)試器使用這個命令來裝載擴展DLL
0:004> .loadby sos mscorwks
這告訴調(diào)試器在裝載mscorwks.dll的地方對擴展”sos.dll”進行裝載。那樣可以保證你獲得SOS的正確版本(這個文件應(yīng)該與你正在使用的mscorwks文件相匹配)。
步驟5:獲取CLR內(nèi)存信息
這個命令用于顯示我們所分配的內(nèi)存信息。你所得到的輸出信息可能和我的不一樣,這主要取決于你所用的版本。但對一個簡單的應(yīng)用程序來說,你將會得到加載器堆棧的兩個基本域結(jié)構(gòu)(他們維持著以排序方式進行共享的對象)和存儲器的第一個真實應(yīng)用程序域(Domain 1)。當然還有被jit處理的代碼。
0:004> !EEHeap
Loader Heap:
--------------------------------------
System Domain: 5e093770
...etc...
Total size: 0x8000(32768)bytes
--------------------------------------
Shared Domain: 5e093fa8
...etc...
Total size: 0xa000(40960)bytes
--------------------------------------
Domain 1: 14f0d0
...etc...
Total size: 0x18000(98304)bytes
--------------------------------------
Jit code heap:
LoaderCodeHeap: 02ef0000(10000:7000) Size: 0x7000(28672)bytes.
Total size: 0x7000(28672)bytes
--------------------------------------
Module Thunk heaps:
...etc...
Total size: 0x0(0)bytes
--------------------------------------
Module Lookup Table heaps:
...etc...
Total size: 0x0(0)bytes
--------------------------------------
Total LoaderHeap size: 0x31000(200704)bytes
我們可以看到在內(nèi)存被所加載的實體占用了200K,被jit處理的代碼占用了28K.
接下來是GC堆棧的輸出信息。
=======================================
Number of GC Heaps: 1
generation 0 starts at 0x00a61018
generation 1 starts at 0x00a6100c
generation 2 starts at 0x00a61000
ephemeral segment allocation context: none
segment begin allocated size
001b8630 7a8d0bbc 7a8f08d8 0x0001fd1c(130332)
001b4ac8 7b4f77e0 7b50dcc8 0x000164e8(91368)
00157690 02c10004 02c10010 0x0000000c(12)
00157610 5ba35728 5ba7c4a0 0x00046d78(290168)
00a60000 00a61000 00aac000 0x0004b000(307200)
Large object heap starts at 0x01a61000
segment begin allocated size
01a60000 01a61000 01a66d90 0x00005d90(23952)
Total Size 0xcdd18(843032)
------------------------------
GC Heap Size 0xcdd18(843032)
在你的屏幕上可能會有很多更小的堆棧片斷,這是因為我是在內(nèi)部調(diào)試構(gòu)造上進行試驗的,本來在轉(zhuǎn)儲時會出現(xiàn)很多象12字節(jié)的小片斷。這樣你就可以知道那些片斷究竟是什么?他們到底有多大?還可以計算出當前內(nèi)存的確切大小。
從上圖我們可以看到GC堆棧的大小為843K。而其他的數(shù)據(jù)共占了2M,其中CLR就占用了1M左右,剩下就是一些從winforms應(yīng)用程序分配出來的位圖
步驟6:轉(zhuǎn)儲GC堆棧統(tǒng)計數(shù)據(jù)
接下來我們需要知道在這個具體實例的堆棧中到底有些什么類型
0:004> !DumpHeap -stat
... sorted from smallest to biggest ... etc. etc...
7b586c7c 436 10464 System.Internal.Gdi.WindowsGraphics
5ba867ac 208 11648 System.Reflection.RuntimeMethodInfo
7b586898 627 12540 System.Internal.Gdi.DeviceContext
5baa4954 677 39992 System.Object[]
5ba25c9c 8593 561496 System.String
Total 17427 objects
注意,如果你不知道在你執(zhí)行這條命令之前GC是否已經(jīng)運行,那么以上數(shù)據(jù)中將可能會包括可達和不可達這兩種對象。而且在這張表中你還可以看到一些已終止的對象。有時,在執(zhí)行該命令之前強制運行GC來得到當前存活對象的內(nèi)存信息是很有用的。也可以通過在運行GC前后將內(nèi)存信息進行轉(zhuǎn)儲來判斷哪些類別的對象已死亡。
我們假設(shè)此時不應(yīng)該為208 System.Reflection.RuntimeMethodInfo對象分配內(nèi)存,即我們就可以認為這是一個內(nèi)存泄漏?,F(xiàn)在我們需要做的一件事就是使用CLR Profiler來查看這些對象是在哪里進行分配的 --所得到的信息將會充滿半個屏幕。但我們可以在該調(diào)試器中得到一些其他的重要信息。
步驟7:轉(zhuǎn)儲特殊類型的信息
我們可以使用一個簡單的命令來轉(zhuǎn)儲每一個包含給定字符類型的對象
0:004> !DumpHeap -type System.Reflection.RuntimeMethodInfo
Address MT Size
00a63da4 5baa62c0 32
00a63e04 5baa6174 20
00a63e2c 5ba867ac 56
00a63e64 5baa5fa8 16
00a63e88 5baa5fa8 16
00a63f24 5baa6174 20
00a63f4c 5ba867ac 56
00a63f84 5baa5fa8 16
etc. etc. etc.
total 630 objects
Statistics:
MT Count TotalSize Class Name
5baa62c0 3 96 System.RuntimeType+RuntimeTypeCache+MemberInfoCache`1
[[System.Reflection.RuntimeMethodInfo, mscorlib]]
5baa5fa8 211 3376 System.Reflection.CerArrayList`1
[[System.Reflection.RuntimeMethodInfo, mscorlib]]
5baa6174 208 4160 System.Collections.Generic.List`1
[[System.Reflection.RuntimeMethodInfo, mscorlib]]
5ba867ac 208 11648 System.Reflection.RuntimeMethodInfo
Total 630 objects
注意,我們所需要的類型是System.Reflection.RuntimeMethodInfo,顯示的內(nèi)容中含有一個方法表,其中包括5ba867ac,它所對應(yīng)的都是一些56字節(jié)的對象?,F(xiàn)在我們就可以來研究他們,看看是什么讓他們保持存活狀態(tài)。
步驟8:確定可疑泄漏的根源
轉(zhuǎn)儲中有一行是
00a63e2c 5ba867ac 56
因此我們可以知道我們要找的對象所在的地址是00a63e2c。接下來看是什么讓他們保持存活狀態(tài)。
0:004> !gcroot 00a63e2c
Scan Thread 0 OSTHread 1598
Scan Thread 2 OSTHread 103c
DOMAIN(0014F0D0):
HANDLE(WeakLn):3f10f0:
Root:00a63d20(System.RuntimeType+RuntimeTypeCache)
->00a63da4(System.RuntimeType+RuntimeTypeCache+MemberInfoCache`1
[[System.Reflection.RuntimeMethodInfo,mscorlib]])
->00a63e88(System.Reflection.CerArrayList`1
[[System.Reflection.RuntimeMethodInfo, mscorlib]])
->00a63e98(System.Object[])
->00a63e2c(System.Reflection.RuntimeMethodInfo)
DOMAIN(0014F0D0):
HANDLE(Pinned):3f13ec:
Root:01a64b50(System.Object[])
->00a62f20(System.ComponentModel.WeakEventHandlerList)
->00a63fb4(System.ComponentModel.WeakEventHandlerList+ListEntry)
->00a63ec4(System.ComponentModel.WeakEventHandlerList+ListEntry)
->00aa5f6c(System.ComponentModel.WeakDelegateHolder)
->00a63e2c(System.Reflection.RuntimeMethodInfo)
在以上內(nèi)容中我加入了一些附加的行以方便我們閱讀。
Gcroot命令告訴你該對象是否可達,如果可達則告訴你如何從每個root到達該對象。這種轉(zhuǎn)儲將不會包含所有到達該對象的路徑,但至少包含一條可以找到該對象的路徑 – 通常這已經(jīng)足夠了。如果轉(zhuǎn)儲中出現(xiàn)了多條路徑,那么這些路徑都含有相同的尾部。如果該對象可達(說明該對象可能只有弱引用存在,因此在下一次查看時該對象就不會再出現(xiàn)了),你就應(yīng)該從剩下的引用中找到相關(guān)的線索。從那些引用中你就可以決定哪些指針需要指向空來釋放對象所占用的內(nèi)存。