一:背景
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