一:背景
1. 讲故事
前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信赖的,这次另一个程序又遇到内存透露,心愿我帮忙诊断下。
其实这位老哥技术还是很不错的,他既然能给我dump,那真的是遇到很辣手的疑难杂症了,我得做好心理准备,沟通下来大略就是程序的内存会迟缓收缩,直到自毁,问题就是这么一个问题,接下来祭出我的看家工具 windbg。
二: windbg 剖析
1. 到底哪里透露了?
我在之前很多篇文章中都说过,遇到这种内存透露,首先就要排查到底是 托管堆
还是 非托管堆
的问题 ?如果是后者,大多数状况只能举手投降,因为这外面水太深了。。。 别看那些案例用 AllocHGlobal
办法调配非托管内存,而后用 !heap 去找的小儿科,现实情况比这种要简单的多。。。
接下来先用 !address -summary
看一下以后过程的提交内存。
0:000> !address -summary--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotalFree 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 %ofTotalMEM_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 %ofTotalMEM_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 -gcNumber of GC Heaps: 1generation 0 starts at 0x0000027795928060generation 1 starts at 0x000002779572F0D0generation 2 starts at 0x000002763DCE1000Total 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 00007ffe2b1f2aa0Name: Unknown ModuleAttributes: Reflection SupportsUpdateableMethods IsDynamic IsInMemory Assembly: 000002776c1d8470BaseAddress: 0000000000000000PEFile: 000002776C1D8BF0ModuleId: 00007FFE2B1F2EB8ModuleIndex: 00000000000177CFLoaderHeap: 0000000000000000TypeDefToMethodTableMap: 00007FFE2B1EE8C0TypeRefToMethodTableMap: 00007FFE2B1EE8E8MethodDefToDescMap: 00007FFE2B1EE910FieldDefToDescMap: 00007FFE2B1EE960MemberRefToDescMap: 0000000000000000FileReferencesMap: 00007FFE2B1EEA00AssemblyReferencesMap: 00007FFE2B1EEA28
我去,BaseAddress 竟然没有地址,真晦气,这也就是说该 module 你是无奈导出的,想想也对,毕竟是动静生成的,可能写代码的人都搞不清楚module中是什么?难道真的就没有方法了吗? 可俗话说得好,天无绝人之路,在 !dumpmodule
命令中有一个 mt (methodtable) 参数,用来显示以后module中都有哪些类型,这就是重大线索。
||2:2:152> !dumpmodule -mt 00007ffe2b1f2aa0 Name: Unknown ModuleAttributes: Reflection SupportsUpdateableMethods IsDynamic IsInMemory Assembly: 000002776c1d8470Types 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.Object00007ffdbaed3730 0x02000002 Castle.DynamicProxy.IProxyTargetAccessor00007ffdbaec8f98 0x02000003 Castle.DynamicProxy.ProxyGenerationOptions00007ffdbaec7fe8 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