一:背景

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

记一次 .NET 某HIS系统后端服务 内存泄漏分析的更多相关文章

  1. 记一次 .NET 某招聘网后端服务 内存暴涨分析

    一:背景 1. 讲故事 前段时间有位朋友wx找到我,说他的程序存在内存阶段性暴涨,寻求如何解决,和朋友沟通下来,他的内存平时大概是5G 左右,在某些时点附近会暴涨到 10G+, 画个图大概就是这样. ...

  2. 记一次 .NET 某消防物联网 后台服务 内存泄漏分析

    一:背景 1. 讲故事 去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了...截图如下: 时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了 二 ...

  3. 记一次 WinDbg 分析 .NET 某工厂MES系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友加微信求助,说他的程序跑着跑着就内存爆掉了,寻求如何解决,截图如下: 从聊天内容看,这位朋友压力还是蛮大的,话说这貌似是我分析的第三个 MES 系统了,看样子 . ...

  4. 记一次 .NET 某智能服装智造系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下: 朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧. 二:Windbg 分析 1. ...

  5. 记一次 .NET 某电厂Web系统 内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...

  6. 记一次某制造业ERP系统 CPU打爆事故分析

    一:背景 1.讲故事 前些天有位朋友微信找到我,说他的程序出现了CPU阶段性爆高,过了一会就下去了,咨询下这个爆高阶段程序内部到底发生了什么? 画个图大概是下面这样,你懂的. 按经验来说,这种情况一般 ...

  7. 记一次 .NET 某企业OA后端服务 卡死分析

    一:背景 1.讲故事 前段时间有位朋友微信找到我,说他生产机器上的 Console 服务看起来像是卡死了,也不生成日志,对方也收不到我的httpclient请求,不知道程序出现什么情况了,特来寻求帮助 ...

  8. 记一次 .NET医疗布草API程序 内存暴涨分析

    一:背景 1. 讲故事 我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS ...

  9. 记一次 .NET 某外贸Web站 内存泄漏分析

    一:背景 1. 讲故事 上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬. 沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了 ...

随机推荐

  1. [枚举]P1089 津津的储蓄计划

    津津的储蓄计划 题目描述 津津的零花钱一直都是自己管理.每个月的月初妈妈给津津300元钱,津津会预算这个月的花销,并且总能做到实际花销和预算的相同. 为了让津津学习如何储蓄,妈妈提出,津津可以随时把整 ...

  2. MySQL提升笔记(1):MySQL逻辑架构

    深入学习MySQL,从概览MySQL逻辑架构开始. 首先来看一下MySQL的逻辑架构图: MySQL逻辑架构大概可以分为三层: 客户端:最上层的服务并不是MySQL所独有的,大多数基于网络的客户端/服 ...

  3. 如何使用Excel发送邮件?

    假设你有一个Excel,其中列出了所有收件人的信息,如下所示: 如果需要向列表中的每个用户发送一封邮件,最好使用当前记录生成一个附件,并且格式如下: 姓名, 发送消息 你应该怎么办?一个一个拷贝发送? ...

  4. AutoAssign源码分析

    目录 AutoAssign源码分析 一. 简介 二. 论文理论 2.1 联合表示 2.2 正样本权重 2.3 负样本权重 2.4 总的loss 2.5 补充loss 三. 论文代码 四. 总结 五. ...

  5. 如何讲一个网页转换为jpg?(图片!)

    不需要安装插件!!! 打开网页,打开开发者工具 快捷键: ctrl+shift+p输入>full即可自动下载!

  6. 「新特性」Spring Boot 全局懒加载机制了解一下

    关于延迟加载 在 Spring 中,默认情况下所有定的 bean 及其依赖项目都是在应用启动时创建容器上下文是被初始化的.测试代码如下: @Slf4j @Configuration public cl ...

  7. 2、ASP.NET Core中服务的生命周期

    ASP.NET Core支持依赖注入软件设计模式,它允许在不同的组件中注入我们的服务,并且控制服务的初始化.有些服务可以在短时间内初始化,并且只能在某个特别的组件,以及请求中才能用到:而还有一些服务, ...

  8. src/众测篇:oracle注入过滤-- , + - * /,case when ,select,from,decode等函数如何证明是注入?

    (1)nullif: NULLIF:如果exp1和exp2相等则返回空(NULL),否则返回第一个值 真: 假: (2)nvl/nvl2 测试失败无法实现:)记录   (3)如果是oracle报错注入 ...

  9. E - Level K Palindrome

    题目大意: As a token of his gratitude, Takahashi has decided to give Snuke a level-KK palindrome. A leve ...

  10. 03- web表单测试

    软件分为 b/s c/s两种架构 表单测试 1.用户注册,登录,信息提交. 2.用户查询商品. 3.用户订购商品. 4.用户查询订单等. 表单测试实例 表单数据添加测试(一) 添加按钮可用,测试点击添 ...