一:背景

1. 讲故事

上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬。

沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了托管堆上有 10w+ 的 byte[] 数组,并占用了大概 1.1G 的内存,在抽取几个 byte[] 的 gcroot 后发现没有引用,接下来就排查不下去了,虽然知道问题可能在 byte[],但苦于找不到证据。

那既然这么信任的找到我,我得要做一个相对全面的输出报告,不能辜负大家的信任哈,还是老规矩,上 windbg 说话。

二: windbg 分析

1. 排查泄漏源

看过我文章的老读者应该知道,排查这种内存泄露的问题,首先要二分法找出到底是托管还是非托管出的问题,方便后续采取相应的应对措施。

接下来使用 !address -summary 看一下进程的提交内存。


||2:2:080> !address -summary --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE 573 1`5c191000 ( 5.439 GB) 95.19% 0.00%
MEM_IMAGE 1115 0`0becf000 ( 190.809 MB) 3.26% 0.00%
MEM_MAPPED 44 0`05a62000 ( 90.383 MB) 1.54% 0.00% --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 201 7ffe`9252e000 ( 127.994 TB) 100.00%
MEM_COMMIT 1477 0`d439f000 ( 3.316 GB) 58.04% 0.00%
MEM_RESERVE 255 0`99723000 ( 2.398 GB) 41.96% 0.00%

从卦象的 MEM_COMMIT 指标看:当前只有 3.3G 的内存占用,说实话,我一般都建议 5G+ 是做内存泄漏分析的最低门槛,毕竟内存越大,越容易分析,接下来看一下托管堆的内存占用。


||2:2:080> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000002b37c0c48
generation 1 starts at 0x00000002b3781000
generation 2 starts at 0x0000000000cc1000 ------------------------------
GC Heap Size: Size: 0xbd322bb0 (3174181808) bytes.

可以看到,当前托管堆占用 3174181808/1024/1024/1024= 2.95G,哈哈,看到这个数,心里一阵狂喜,托管堆上的问题,对我来说差不多就十拿九稳了。。。毕竟还没有失手过,接下来赶紧排查一下托管堆,看下是哪里出的问题。

2. 查看托管堆

要想查看托管堆,可以使用 !dumpheap -stat 命令,下面我把 Top10 Size 给显示出来。


||2:2:080> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007ffd7e130ab8 116201 13014512 Newtonsoft.Json.Linq.JProperty
00007ffdd775e560 66176 16411648 System.Data.SqlClient._SqlMetaData
00007ffddbcc9da8 68808 17814644 System.Int32[]
00007ffddbcaf788 14140 21568488 System.String[]
00007ffddac72958 50256 22916736 System.Net.Sockets.SocketAsyncEventArgs
00007ffd7deb64b0 369 62115984 System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.ICustomAttributeProvider, mscorlib],[System.Type, mscorlib]][]
00007ffddbcc8610 8348 298313756 System.Char[]
00007ffddbcc74c0 1799807 489361500 System.String
000000000022e250 312151 855949918 Free
00007ffddbccc768 109156 1135674368 System.Byte[]

从上面的输出中可以看到,当前状元是 Byte[],榜眼是 Free,探花是 String,这里还是有一些经验之谈的,深究 Byte[]String 这种基础类型,投入产出比是不高的,毕竟大量的复杂类型,它的内部结构都含有 String 和 Byte[],比如我相信 MemoryStream 内部肯定有 Byte[],对吧,所以暂且放下状元和探花,看一下榜眼或者其他的复杂类型。

如果你的眼睛犀利,你会发现 Free 的个数有 31W+,你肯定想问这是什么意思?对,这表明当前托管堆上有 31W+ 的空闲块,它的专业术语叫 碎片化,所以这条信息透露出了当前托管堆有相对严重的碎片化现象,接下来的问题就是为什么会这样? 大多数情况出现这种碎片化的原因在于托管堆上有很多的 pinned 对象,这种对象可以阻止 GC 在回收时对它的移动,长此以往就会造成托管堆的支离破碎,所以找出这种现象对解决泄漏问题有很大的帮助。

补充一下,这里可以借助 dotmemory ,红色表示 pinned 对象,肉眼可见的大量的红色间隔分布,最后的碎片率为 85% 。

接下来的问题是如何找到这些 pinned 对象,其实在 CLR 中有一张 GCHandles 表,里面就记录了这些玩意。

3. 查看 GCHandles

要想找到所有的 pinned 对象,可以使用 !gchandles -stat 命令,简化输出如下:


||2:2:080> !gchandles -stat
Statistics:
MT Count TotalSize Class Name
00007ffddbcc88a0 278 26688 System.Threading.Thread
00007ffddbcb47a8 1309 209440 System.RuntimeType+RuntimeTypeCache
00007ffddbcc7b38 100 348384 System.Object[]
00007ffddbc94b60 9359 673848 System.Reflection.Emit.DynamicResolver
00007ffddb5b7b98 25369 2841328 System.Threading.OverlappedData
Total 36566 objects Handles:
Strong Handles: 174
Pinned Handles: 15
Async Pinned Handles: 25369
Ref Count Handles: 1
Weak Long Handles: 10681
Weak Short Handles: 326

从卦象中可以看出,当前有一栏为: Async Pinned Handles: 25369 ,这表示当前有 2.5w 的异步操作过程中被pinned住的对象,这个指标就相当不正常了,而且可以看出与 2.5W 的System.Threading.OverlappedData 遥相呼应,有了这个思路,可以回过头来看一下托管堆,是否有相对应的 2.5w 个类似封装过异步操作的复杂类型对象? 这里我再把 top10 Size 的托管堆列出来。


||2:2:080> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007ffd7e130ab8 116201 13014512 Newtonsoft.Json.Linq.JProperty
00007ffdd775e560 66176 16411648 System.Data.SqlClient._SqlMetaData
00007ffddbcc9da8 68808 17814644 System.Int32[]
00007ffddbcaf788 14140 21568488 System.String[]
00007ffddac72958 50256 22916736 System.Net.Sockets.SocketAsyncEventArgs
00007ffd7deb64b0 369 62115984 System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.ICustomAttributeProvider, mscorlib],[System.Type, mscorlib]][]
00007ffddbcc8610 8348 298313756 System.Char[]
00007ffddbcc74c0 1799807 489361500 System.String
000000000022e250 312151 855949918 Free
00007ffddbccc768 109156 1135674368 System.Byte[]

有了这种先入为主的思想,我想你肯定发现了托管堆上的这个 50256 的 System.Net.Sockets.SocketAsyncEventArgs,看样子这回泄漏和 Socket 脱不了干系了,接下来可以查下这些 SocketAsyncEventArgs 到底被谁引用着?

4. 查看 SocketAsyncEventArgs 引用根

要想查看引用根,先从 SocketAsyncEventArgs 中导几个 address 出来。


||2:2:080> !dumpheap -mt 00007ffddac72958 0 0000000001000000
Address MT Size
0000000000cc9dc0 00007ffddac72958 456
0000000000ccc0d8 00007ffddac72958 456
0000000000ccc358 00007ffddac72958 456
0000000000cce670 00007ffddac72958 456
0000000000cce8f0 00007ffddac72958 456
0000000000cd0c08 00007ffddac72958 456
0000000000cd0e88 00007ffddac72958 456
0000000000cd31a0 00007ffddac72958 456
0000000000cd3420 00007ffddac72958 456
0000000000cd5738 00007ffddac72958 456
0000000000cd59b8 00007ffddac72958 456
0000000000cd7cd0 00007ffddac72958 456

然后查看第一个和第二个address的引用根。


||2:2:080> !gcroot 0000000000cc9dc0
Thread 86e4:
0000000018ecec20 00007ffd7dff06b4 xxxHttpServer.DaemonThread`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].DaemonThreadStart()
rbp+10: 0000000018ececb0
-> 000000000102e8c8 xxxHttpServer.DaemonThread`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 00000000010313a8 xxxHttpServer.xxxHttpRequestServer`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 000000000105b330 xxxHttpServer.HttpSocketTokenPool`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 000000000105b348 System.Collections.Generic.Stack`1[[xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]], xxxHttpServer]]
-> 0000000010d36178 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]][]
-> 0000000008c93588 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 0000000000cc9dc0 System.Net.Sockets.SocketAsyncEventArgs
||2:2:080> !gcroot 0000000000ccc0d8
Thread 86e4:
0000000018ecec20 00007ffd7dff06b4 xxxHttpServer.DaemonThread`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].DaemonThreadStart()
rbp+10: 0000000018ececb0
-> 000000000102e8c8 xxxHttpServer.DaemonThread`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 00000000010313a8 xxxHttpServer.xxxHttpRequestServer`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 000000000105b330 xxxHttpServer.HttpSocketTokenPool`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 000000000105b348 System.Collections.Generic.Stack`1[[xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]], xxxHttpServer]]
-> 0000000010d36178 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]][]
-> 0000000000ccc080 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
-> 0000000000ccc0d8 System.Net.Sockets.SocketAsyncEventArgs

从输出信息看,貌似程序自己搭了一个 HttpServer,还搞了一个 HttpSocketTokenPool 池,好奇心来了,把这个类导出来看看怎么写的?

5. 寻找问题代码

还是老办法,使用 !savemodule 导出问题代码,然后使用 ILSpy 进行反编译。

说实话,这个 pool 封装的挺简陋的,既然 SocketAsyncEventArgs 有 5W+,我猜测这个 m_pool 池中估计也得好几万,为了验证思路,可以用 windbg 把它挖出来。

从图中的size可以看出,这个 pool 有大概 2.5w 的 HttpSocket,这就说明这个所谓的 Socket Pool 其实并没有封装好。

三:总结

想自己封装一个Pool,得要实现一些复杂的逻辑,而不能仅仅是一个 PUSH 和 POP 就完事了。。。 所以优化方向也很明确,想办法控制住这个 Pool,实现 Pool 该实现的效果。

更多高质量干货:参见我的 GitHub: dotnetfly

记一次 .NET 某外贸Web站 内存泄漏分析的更多相关文章

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

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

  2. 记一次 .NET 某旅行社Web站 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下. 可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子..., ...

  3. 记一次 .NET 某电商交易平台Web站 CPU爆高分析

    一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...

  4. 记一次 .NET 某HIS系统后端服务 内存泄漏分析

    一:背景 1. 讲故事 前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下. 其实这位老哥技术还是很不错的,他既然 ...

  5. 记一次 .NET 某智慧水厂API 非托管内存泄漏分析

    一:背景 1. 讲故事 七月底的时候有位朋友在wx上找到我,说他的程序内存占用8G,托管才占用1.5G,询问剩下的内存哪里去了?截图如下: 从求助内容看,这位朋友真的太客气了,动不动就谈钱,真伤感情, ...

  6. 记一次 .NET 某流媒体独角兽 API 句柄泄漏分析

    一:背景 1. 讲故事 上上周有位朋友找到我,说他的程序CPU和句柄都在不断的增长,无回头趋势,查了好些天也没什么进展,特加wx寻求帮助,截图如下: 看的出来这位朋友也是非常郁闷,出问题还出两个,气人 ...

  7. 记一次 .NET 某桌面奇侠游戏 非托管内存泄漏分析

    一:背景 1. 讲故事 说实话,这篇dump我本来是不准备上一篇文章来解读的,但它有两点深深的感动了我. 无数次的听说用 Unity 可做游戏开发,但百闻不如一见. 游戏中有很多金庸武侠小说才有的名字 ...

  8. 记一次 .NET 某风控管理系统 内存泄漏分析

    一:背景 1. 讲故事 上个月中旬,星球里的一位朋友在微信找我,说他的程序跑着跑着内存会不断的缓慢增长并无法释放,寻求如何解决 ? 得,看样子星球还得好好弄!!! 不管怎么说,先上 windbg 说话 ...

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

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

随机推荐

  1. Baolu Aggregate TPS Report

    1.说明 这是一个基于JMeter官方的Aggregate Report的监听器改进而来的监听器!!! 2.插件背景 早在很久之前,宝路就曾经改造过JMeter的Aggregate Report 的源 ...

  2. iNeuOS工业互联平台,生产过程业务联动控制

    1.概述 工业物联网也好.工业互联网也好或是其他生产系统,反向控制始终无法回避.搞工业最直接.最体现效果的两个方面是采集各种数据和生产过程业务控制,所谓大数据预测和分析,那是仁者见仁.智者见智,下一篇 ...

  3. 了解 Vue 的 Compsition API

    在这篇文章中,我将讲讲 Vue 的 Composition API 为什么比之前的 Options API 要好,以及它是如何工作的. Options API 有什么问题 首先,这里不是要大家放弃 O ...

  4. Kubernetes 用户流量接入方案

    总结Kubernetes 生产环境用户流量接入方案 方案1 client -> ddos -> waf -> slb 7层域名 -> nginx端口 -> ingress ...

  5. 如何自己设计一个类似dubbo的rpc框架?

    (1)上来你的服务就得去注册中心注册吧,你是不是得有个注册中心,保留各个服务的信息,可以用zookeeper来做,对吧 (2)然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存 ...

  6. 使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神

    使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神 前言 接上一篇 使用 EPPlus 封装的 excel 表格导入功能 (一) 前一篇的是大概能用但是 ...

  7. 【Azure Developer】调用SDK的runPowerShellScript方法,在Azure VM中执行PowerShell脚本示例

    当需要通过代码的方式执行PowerShell脚本时,可以参考以下的示例. Azure SDK中提供了两个方法来执行PowerShell脚本 (SDK Source Code: https://gith ...

  8. 干货满满 AppGallery Connect研习社·直播深度解析优质应用开发流程

  9. 005-Java中的控制语句

    目录 一.控制语句 一.作用 二.分类 二.选择语句(分支语句) 一.if 语句 二.switch语句 三.循环语句 一.for循环 二.while循环(while循环的循环次数是:0~n次) 三.d ...

  10. 【Spring】循环依赖

    @ 目录 循环依赖 是什么? Spring是如何解决的? 源码分析 细节 循环依赖 是什么? ​ 简单的来说就是对象a的属性中引用了对象b,对象b的属性中引用了对象c......最后引用到a. < ...