一:背景
1. 講故事
前天那位 his 老哥又來找我了,上次因為CPU爆高的問題我給解決了,看樣子對我挺信任的,這次另一個程式又遇到記憶體洩漏,希望我幫忙診斷下。
其實這位老哥技術還是很不錯的,他既然能給我dump,那真的是遇到很棘手的疑難雜症了???,我得做好心理準備???,溝通下來大概就是程式的記憶體會緩慢膨脹,直到自毀,問題就是這麼一個問題,接下來祭出我的看家工具 windbg。
二: windbg 分析
1. 到底哪裡洩漏了?
我在之前很多篇文章中都說過,遇到這種記憶體洩漏,首先就要排查到底是 託管堆
還是 非託管堆
的問題 ?如果是後者,大多數情況只能舉手投降,因為這裡面水太深了。。。 別看那些案例用 AllocHGlobal
方法分配非託管記憶體,然後用 !heap 去找的小兒科,現實情況比這種要複雜的多。。。
接下來先用 !address -summary
看一下當前程式的提交記憶體。
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 345 7dfd`ca3ca000 ( 125.991 TB) 98.43%
<unknown> 37399 201`54dbf000 ( 2.005 TB) 99.83% 1.57%
Heap 29887 0`d179b000 ( 3.273 GB) 0.16% 0.00%
Image 1312 0`0861b000 ( 134.105 MB) 0.01% 0.00%
Stack 228 0`06e40000 ( 110.250 MB) 0.01% 0.00%
Other 10 0`001d8000 ( 1.844 MB) 0.00% 0.00%
TEB 76 0`00098000 ( 608.000 kB) 0.00% 0.00%
PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00%
--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED 352 200`00a40000 ( 2.000 TB) 99.57% 1.56%
MEM_PRIVATE 67249 2`2cbcb000 ( 8.699 GB) 0.42% 0.01%
MEM_IMAGE 1312 0`0861b000 ( 134.105 MB) 0.01% 0.00%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 345 7dfd`ca3ca000 ( 125.991 TB) 98.43%
MEM_RESERVE 11805 200`22ae8000 ( 2.001 TB) 99.60% 1.56%
MEM_COMMIT 57108 2`1313e000 ( 8.298 GB) 0.40% 0.01%
從卦象上看, 程式提交記憶體 MEM_COMMIT = 8.2G
, 然後我們看下託管堆大小,使用 !eeheap -gc
命令。
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000027795928060
generation 1 starts at 0x000002779572F0D0
generation 2 starts at 0x000002763DCE1000
Total Size: Size: 0xcd28c510 (3442001168) bytes.
------------------------------
GC Heap Size: Size: 0xcd28c510 (3442001168) bytes.
從最後一行可以看出,當前的GC堆 Size= 3442001168 /1024/1024/1024 =3.2G
,也就是說大概: 8.2G - 3.2G = 5G
的記憶體丟掉了。。。尼瑪,典型的 非託管記憶體洩漏
,真的是哪壺不開提哪壺,這下可能真的要栽了。。。
2. 尋找非託管記憶體洩漏
除了 GC 堆,程式裡面還有一個叫做 loader 堆,這裡面東西就多了,有高頻堆,低頻堆,Stub堆,JIT堆 等等,存放著和 AppDomain,Module,方法描述符,方法表,EEClass 等相關資訊,從經驗來說,這個 loader 堆是考察 非託管洩漏
優先考慮的地方,要想檢視,可使用 !eeheap -loader
命令。
0:000> !eeheap -loader
...
Module 00007ffe2b1b6ca8: Size: 0x0 (0) bytes.
Module 00007ffe2b1b7e80: Size: 0x0 (0) bytes.
Module 00007ffe2b1b9058: Size: 0x0 (0) bytes.
Module 00007ffe2b1ba230: Size: 0x0 (0) bytes.
Module 00007ffe2b1bb408: Size: 0x0 (0) bytes.
Module 00007ffe2b1bc280: Size: 0x0 (0) bytes.
Module 00007ffe2b1bd458: Size: 0x0 (0) bytes.
Module 00007ffe2b1be630: Size: 0x0 (0) bytes.
Module 00007ffe2b1bf808: Size: 0x0 (0) bytes.
Module 00007ffe2b1f0a50: Size: 0x0 (0) bytes.
Module 00007ffe2b1f1c28: Size: 0x0 (0) bytes.
Module 00007ffe2b1f2aa0: Size: 0x0 (0) bytes.
Total size: Size: 0x0 (0) bytes.
--------------------------------------
Total LoaderHeap size: Size: 0xc0fb9000 (3237711872) bytes total, 0x5818000 (92372992) bytes wasted.
這命令不輸還好,一輸嚇一跳,windbg 介面刷了好幾分鐘才停下來。。。 從輸出中可以得到兩點資訊:
-
loader堆 總共佔用:
3237711872 /1024/1024/1024 = 3.01G
-
有非常多的 module 產生,我估計有幾萬個。。。
為了滿足好奇心,我決定寫一個小指令碼看看到底有多少個 module ???
我去,module居然有19w之多,難怪佔用了 3 個多G,感覺離真相不遠了,接下來的問題是這些module是什麼,從哪裡來???
3. 尋找 module 的源頭
要想尋找源頭,大家可以仔細想一想, module 的巢狀關係應該是: Module -> Assembly -> Appdomain
,所以查 AppDomain 或許能給我們更多的資訊,接下來使用 !DumpDomain
匯出當前程式的所有應用程式域,又是刷刷刷的幾分鐘,哎。。。 截圖如下:
從圖中可以看出有大量的 Dynamic
型別的程式集,你肯定想問這是什麼意思? 對,這就是程式碼動態建立的程式集,居然高達 19w 。。。接下來要解決的一個問題是:這些 Assembly 是怎麼建立出來的???
4. 匯出 module 內容
老讀者應該知道我是怎麼從 module 中匯出問題程式碼的,對,就是尋找 module 的 startaddress,這裡我就挑選其中一個module:00007ffe2b1f2aa0。
2:2:152> !dumpmodule 00007ffe2b1f2aa0
Name: Unknown Module
Attributes: Reflection SupportsUpdateableMethods IsDynamic IsInMemory
Assembly: 000002776c1d8470
BaseAddress: 0000000000000000
PEFile: 000002776C1D8BF0
ModuleId: 00007FFE2B1F2EB8
ModuleIndex: 00000000000177CF
LoaderHeap: 0000000000000000
TypeDefToMethodTableMap: 00007FFE2B1EE8C0
TypeRefToMethodTableMap: 00007FFE2B1EE8E8
MethodDefToDescMap: 00007FFE2B1EE910
FieldDefToDescMap: 00007FFE2B1EE960
MemberRefToDescMap: 0000000000000000
FileReferencesMap: 00007FFE2B1EEA00
AssemblyReferencesMap: 00007FFE2B1EEA28
我去,BaseAddress 居然沒有地址,真倒黴,這也就是說該 module 你是無法匯出的,想想也對,畢竟是動態生成的,可能寫程式碼的人都搞不清楚module中是什麼?難道真的就沒有辦法了嗎? 可俗話說得好,天無絕人之路???,在 !dumpmodule
命令中有一個 mt (methodtable) 引數,用來顯示當前module中都有哪些型別,這就是重大線索。
||2:2:152> !dumpmodule -mt 00007ffe2b1f2aa0
Name: Unknown Module
Attributes: Reflection SupportsUpdateableMethods IsDynamic IsInMemory
Assembly: 000002776c1d8470
Types defined in this module
MT TypeDef Name
------------------------------------------------------------------------------
00007ffe2b1f3168 0x02000002 <Unloaded Type>
00007ffe2b1f2f60 0x02000003 <Unloaded Type>
Types referenced in this module
MT TypeRef Name
------------------------------------------------------------------------------
00007ffdb9f70af0 0x02000001 System.Object
00007ffdbaed3730 0x02000002 Castle.DynamicProxy.IProxyTargetAccessor
00007ffdbaec8f98 0x02000003 Castle.DynamicProxy.ProxyGenerationOptions
00007ffdbaec7fe8 0x02000004 Castle.DynamicProxy.IInterceptor
可以看到module中定義了兩個 type
,都有其方法表地址,接下來通過 mt
來換取 md
(方法描述符) 來得到最後module內容。
到這裡終於就搞清楚了,原來這位老哥是利用 Castle 做了一個 AOP 的功能,應該是沒有正確的使用 AOP ,導致生成了 19w +
的動態程式集,難怪最終會把記憶體給弄爆掉。。。 根子總算找到了,接下來如何去修改呢???
5. 修改 Castle AOP 問題程式碼
這下可把我難住了,畢竟我真的是沒玩過 Castle ???,不過老規矩,到 bing 上看看可有 天涯淪落人
,嘿嘿,還真有 Castle AOP 導致記憶體洩漏的文章:Castle Windsor Interceptor memory leak ,解決辦法也提供了,截圖如下:
趕緊把這篇連結丟給老哥,我感覺也只能幫他到這裡了,剩下的只能看造化。
三:總結
真的是造化弄人,老哥以迅雷不及掩耳之勢就給搞定了,當天晚上就已完成自測上線。
我趕緊追問老哥是怎麼改的???,老哥也不惜把原始碼放出來了,果然按照老外的建議將 ProxyGenerator
設定成 static 就搞定了。。。否則一個new一個assembly,再看看改之前的程式碼,截圖如下:
搞定了這兩個難啃的問題,感覺是不是要發一個小獎盃給我呢????
更多高質量乾貨:參見我的 GitHub: dotnetfly