百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

cac55 2024-10-11 10:52 96 浏览 0 评论

一:背景

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

相关推荐

高中生又来卷我们了!手搓 Android 浏览器,可高度定制+脚本支持

回想一下,你曾经的暑假,是怎么度过的?可能是无尽的娱乐时光,或者是懒洋洋的休息日。然而,对于这位Gitee上的高中生来说,他选择在这个暑假里独立开发一款Android浏览器——Vie浏览器,...

网页加载CAD图纸的两个方案对比说明(网页浏览编辑DWG)

一.说明梦想控件提供两种技术在网页中加载CAD图纸,一个是OCX技术方案,另一个是HTML5技术方案,它们各有优缺点,用户需根据实际情况进行选择,下边分别说明一下。1、ocx技术方案(1)OCX技术是...

前后端分离的开源在线考试系统调试实战

开篇在我们的教育生涯中,或多或少的都接触过在线考试系统。例如大学里最常见的各种软件考试,上机考试等,那么有没有开源的这样的系统呢?当然是有了,今天就来调试个开源的在线考试系统。本文重点是调试,因为很多...

网友:小松鼠长大了!UC浏览器推出18周年专版logo引热议

近日,互联网厂商logo更新再次引发热议。作为国内手机浏览器的代表性厂商,UC浏览器的标志性logo小松鼠悄然发生了变化,在网友中引发了关注和讨论。依照UC微博官方账号的说法,这个全新的形象是UC18...

超多案例!谷歌AI模型Nano Banana的5个实用+趣味玩法

再不用这个AI修图神器,你的同行明天就把订单抢光了。谷歌刚放出的NanoBanana,能在一张照片里把背景、姿势、衣服一次换完,脸还是那张脸。实测把地铁照改成海边大片,只用一句话,三秒出图,不用PS来...

2025年最佳Windows数据恢复软件解决方案前5名

您是否正在寻找互联网上排名前五的WindowsPC最佳数据恢复软件解决方案?其实,网上有很多工具可以恢复已删除的文件。但并非所有应用程序都值得使用。值得信赖的文件恢复工具可以帮助您快速检索丢失、删...

电脑数据恢复软件推荐:10个顶级数据恢复软件分享

在数字化的工作与生活中,电脑文件误删除的情况时有发生,这不仅会引发我们的焦虑情绪,更可能导致重要数据的丢失。不过,幸运的是,借助正确的数据恢复软件,我们仍有机会找回那些被误删的文件。10个顶级数据恢复...

更懂国内APP的开源智能体!感知定位推理中文能力全面提升

更懂国内APP的开源智能体!感知定位推理中文能力全面提升“帮我点外卖,别点到广告位。”一句话,说出了多少人对手机自动化的真实期待。浙大和美团刚扔出来的开源项目UItron,就是冲着这句吐槽来的——它真...

美光首家推出采用EUV技术的1γ DDR5 DRAM芯片

美光科技宣布已开始向部分生态系统合作伙伴和客户出货1γ(1-gamma)16GbitDDR5DRAM芯片。美光声称,它是第一个采用1-gamma(1γ)节点的公司,该节点指的是DRAM工艺技术的第...

DDR4的PCB设计及仿真_ddr pcb

以下文章来源于鼎阳硬件智库,作者王彦武DDR4关键技术和方法分析1.1DDR4与DDR3不同之处相对于DDR3,DDR4首先在外表上就有一些变化,比如DDR4将内存下部设计为中间稍微突出,边缘变...

DDR4和DDR5内存的性能差距有哪些?

DDR4和DDR5内存的性能差距主要体现在带宽、延迟、能效及未来扩展性上,以下是关键差异的总结及选择建议:1.带宽与频率DDR4:主流频率为2133MHz–3600MHz,带宽约25.6–30.2...

DDR5内存一根和两根的区别,建议收藏观看。

大家好,我是海韵,DDR5内存条,单条和双条有什么区别,如何选择,DDR5单条和双条内存在性能上存在差距,单条内存保持在64个通道,但内部升级为32乘以2,虽然出口速度相同,但内部运行略有提升,...

Kingston FURY叛逆者DDR5 RGB CUDIMM内存评测 强势突破9000MT/s!

【ZOL中关村在线原创评测】当8000MT/s从当年的液氮超频艰难达成,到如今XMP轻松开启,DDR5内存频率的极限探索似乎看不到终点。在早先,我们曾为大家带来KingstonFURY品牌的叛逆者D...

SK海力士将在年内推出1bnm 32Gb DDR5内存颗粒

IT之家4月25日消息,据韩媒NEWSIS报道,SK海力士在今日的2024年一季度财报电话会议上表示将在年内推出1bnm32GbDDR5内存颗粒。32Gb颗粒意味着消费级的...

DRAM史上最大代际倒挂继续:三星将延长DDR4生产期限至2026年

IT之家8月6日消息,韩媒TheElec今天(8月6日)发布博文,报道称三星决定延长DDR41zDRAM的生产期限至2026年,一方面在DRAM史上最大代际倒挂中进...

取消回复欢迎 发表评论: