记一次 .NET 某HIS系统后端服务 内存泄漏分析
一:背景
1. 講故事
前天那位 his 老哥又來找我了,上次因為CPU爆高的問題我給解決了,看樣子對我挺信任的,這次另一個程序又遇到內(nèi)存泄漏,希望我?guī)兔υ\斷下。
其實(shí)這位老哥技術(shù)還是很不錯的,他既然能給我dump,那真的是遇到很棘手的疑難雜癥了????????????,我得做好心理準(zhǔn)備????????????,溝通下來大概就是程序的內(nèi)存會緩慢膨脹,直到自毀,問題就是這么一個問題,接下來祭出我的看家工具 windbg。
二:windbg 分析
1. 到底哪里泄漏了?
我在之前很多篇文章中都說過,遇到這種內(nèi)存泄漏,首先就要排查到底是 托管堆 還是 非托管堆 的問題 ?如果是后者,大多數(shù)情況只能舉手投降,因為這里面水太深了。。。別看那些案例用 AllocHGlobal 方法分配非托管內(nèi)存,然后用 !heap 去找的小兒科,現(xiàn)實(shí)情況比這種要復(fù)雜的多。。。
接下來先用 !address -summary 看一下當(dāng)前進(jìn)程的提交內(nèi)存。
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%從卦象上看, 進(jìn)程提交內(nèi)存 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?0x000002763DCE1000Total?Size:??????????????Size:?0xcd28c510?(3442001168)?bytes. ------------------------------ GC?Heap?Size:????Size:?0xcd28c510?(3442001168)?bytes.從最后一行可以看出,當(dāng)前的GC堆 Size= 3442001168 /1024/1024/1024 =3.2G,也就是說大概:8.2G - 3.2G = 5G 的內(nèi)存丟掉了。。。尼瑪,典型的 非托管內(nèi)存泄漏,真的是哪壺不開提哪壺,這下可能真的要栽了。。。
2. 尋找非托管內(nèi)存泄漏
除了 GC 堆,進(jìn)程里面還有一個叫做 loader 堆,這里面東西就多了,有高頻堆,低頻堆,Stub堆,JIT堆 等等,存放著和 AppDomain,Module,方法描述符,方法表,EEClass 等相關(guān)信息,從經(jīng)驗來說,這個 loader 堆是考察 非托管泄漏 優(yōu)先考慮的地方,要想查看,可使用 !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 界面刷了好幾分鐘才停下來。。。從輸出中可以得到兩點(diǎn)信息:
loader堆 總共占用:3237711872 /1024/1024/1024 = 3.01G
有非常多的 module 產(chǎn)生,我估計有幾萬個。。。
為了滿足好奇心,我決定寫一個小腳本看看到底有多少個 module ???
我去,module居然有19w之多,難怪占用了 3 個多G,感覺離真相不遠(yuǎn)了,接下來的問題是這些module是什么,從哪里來???
3. 尋找 module 的源頭
要想尋找源頭,大家可以仔細(xì)想一想, module 的嵌套關(guān)系應(yīng)該是:Module -> Assembly -> Appdomain ,所以查 AppDomain 或許能給我們更多的信息,接下來使用 !DumpDomain 導(dǎo)出當(dāng)前進(jìn)程的所有應(yīng)用程序域,又是刷刷刷的幾分鐘,哎。。。截圖如下:
從圖中可以看出有大量的 Dynamic 類型的程序集,你肯定想問這是什么意思?對,這就是代碼動態(tài)創(chuàng)建的程序集,居然高達(dá) 19w 。。。接下來要解決的一個問題是:這些 Assembly 是怎么創(chuàng)建出來的???
4. 導(dǎo)出 module 內(nèi)容
老讀者應(yīng)該知道我是怎么從 module 中導(dǎo)出問題代碼的,對,就是尋找 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 你是無法導(dǎo)出的,想想也對,畢竟是動態(tài)生成的,可能寫代碼的人都搞不清楚module中是什么?難道真的就沒有辦法了嗎?可俗話說得好,天無絕人之路????????????,在 !dumpmodule 命令中有一個 mt (methodtable) 參數(shù),用來顯示當(dāng)前module中都有哪些類型,這就是重大線索。
||2:2:152>?!dumpmodule?-mt?00007ffe2b1f2aa0? Name:?Unknown?Module Attributes:??????????????Reflection?SupportsUpdateableMethods?IsDynamic?IsInMemory? Assembly:????????????????000002776c1d8470Types?defined?in?this?moduleMT??????????TypeDef?Name ------------------------------------------------------------------------------ 00007ffe2b1f3168?0x02000002?<Unloaded?Type> 00007ffe2b1f2f60?0x02000003?<Unloaded?Type>Types?referenced?in?this?moduleMT????????????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內(nèi)容。
到這里終于就搞清楚了,原來這位老哥是利用 Castle 做了一個 AOP 的功能,應(yīng)該是沒有正確的使用 AOP ,導(dǎo)致生成了 19w + 的動態(tài)程序集,難怪最終會把內(nèi)存給弄爆掉。。。根子總算找到了,接下來如何去修改呢???
5. 修改 Castle AOP 問題代碼
這下可把我難住了,畢竟我真的是沒玩過 Castle ????????????,不過老規(guī)矩,到 bing 上看看可有 天涯淪落人,嘿嘿,還真有 Castle AOP 導(dǎo)致內(nèi)存泄漏的文章:Castle Windsor Interceptor memory leak ,解決辦法也提供了,截圖如下:
趕緊把這篇鏈接丟給老哥,我感覺也只能幫他到這里了,剩下的只能看造化。
三:總結(jié)
真的是造化弄人,老哥以迅雷不及掩耳之勢就給搞定了,當(dāng)天晚上就已完成自測上線。
我趕緊追問老哥是怎么改的????????????,老哥也不惜把源碼放出來了,果然按照老外的建議將 ProxyGenerator 設(shè)置成 static 就搞定了。。。否則一個new一個assembly,再看看改之前的代碼,截圖如下:
搞定了這兩個難啃的問題,感覺是不是要發(fā)一個小獎杯給我呢?????????????
END
工作中的你,是否已遇到 ...?
1. CPU爆高
2. 內(nèi)存暴漲
3. 資源泄漏
4. 崩潰死鎖
5. 程序呆滯
等緊急事件,全公司都指望著你能解決...? 危難時刻才能展現(xiàn)你的技術(shù)價值,作為專注于.NET高級調(diào)試的技術(shù)博主,歡迎微信搜索: 一線碼農(nóng)聊技術(shù),免費(fèi)協(xié)助你分析Dump文件,希望我能將你的踩坑經(jīng)驗分享給更多的人。
總結(jié)
以上是生活随笔為你收集整理的记一次 .NET 某HIS系统后端服务 内存泄漏分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# Hashtable和Diction
- 下一篇: 在非容器(集群)环境下运行dapr