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

一:背景

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 objectsHandles:Strong Handles:       174Pinned Handles:       15Async Pinned Handles: 25369Ref Count Handles:    1Weak Long Handles:    10681Weak 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 0000000001000000Address               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 该实现的效果。

END

工作中的你,是否已遇到 ... 

1. CPU爆高

2. 内存暴涨

3. 资源泄漏

4. 崩溃死锁

5. 程序呆滞

等紧急事件,全公司都指望着你能解决...  危难时刻才能展现你的技术价值,作为专注于.NET高级调试的技术博主,欢迎微信搜索: 一线码农聊技术,免费协助你分析Dump文件,希望我能将你的踩坑经验分享给更多的人。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/302225.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java取整公式,Java取整函数 四舍五入函数-Go语言中文社区

简介Math类中提供了5个与取整相关的函数,如下所示:static double ceil(double a):天花板函数,返回大于等于a的最小整数(但是以浮点数形式存储)。static double floor(double a):地板函数,返回小于等于a的最…

这些优质的教育类公众号您知道么,非常实用!

随着新媒体的发展壮大,教育类微信号林林总总数不胜数,官方的、媒体的、学校的……让人眼花缭乱,不知如何选择,今天,小编要向您推荐一些优质且实用的教育类公众号,请大家立即关注。长按二维码,选…

ML.NET Cookbook:(3)如何从CSV加载包含多个列的数据?

TextLoader用于从文本文件加载数据。您需要指定什么是数据列、它们的类型以及在文本文件中的位置。当输入文件包含许多相同类型的列(通常打算一起使用)时,我们建议从一开始就将它们作为向量列进行加载:这样,数据的架构…

hdu 4267 A Simple Problem with Integers

http://acm.hdu.edu.cn/showproblem.php?pid4267 一道加强版的树状数组题,利用题目的关键点——除数较小,可以想到将除数跟余数分类,最多分成55种情况,也就是每个结点存放55个数据的的树状数组。 建树相对简单,遵循思…

php年月日滚动选择,Unity3d—做一个年月日选择器(Scroll Rect拖动效果优化)— 无限滚动 + 锁定元素...

[导读]最近.....废话不多说上效果图用的是UGUI我先说思路通过判断元素的位置信息来改变Hierarchy的顺序 实现无限滚动改变位置的同时也要不断的调整Content的位置防止乱跳元素锁定就是直接锁死的元素的移动范围 当只有拖动大于一定程度时最近.....废话不多说上效果图用的是UGUI…

3秒取暖,超高颜值!冬日必备的大宇取暖器

天气越来越冷了,在小木冷的瑟瑟发抖的时候,朋友推荐了一台最新款的大宇取暖器,本来我怕是个鸡肋。但颜值确实是小木喜欢的呀,我就让怕冷的朋友先用用看,结果惊讶了!这产品开了一会,朋友的小办公…

.Net Core with 微服务 - 架构图

上一次我们简单介绍了什么是微服务(.NET Core with 微服务 - 什么是微服务)。介绍了微服务的来龙去脉,一些基础性的概念。有大佬在评论区指出说这根本不是微服务。由于本人的能力有限,大概也只能理解到这个层次。先不管它到底是不…

win10+tomcat+php+配置环境变量配置,Win10系统Tomcat环境变量配置方法

在Win10系统中配置Tomcat环境变量之前,需要先配置JAVA,之后就可以配置Tomcat环境了,网络上的教程要么太简单,不明觉厉,要么太复杂,笔者整理了以下思路,便是以下Win10系统Tomcat环境变量配置方法…

136个Python 机器学习知识点让你受益终生!

全世界只有3.14 % 的人关注了数据与算法之美如果村里通了网,那你一定知道【AI】人工智能。如果你会网上冲浪,那你一定看到过【ML】机器学习。小编在网上看到一个段子:ML派坐落美利坚合众山中,百年来武学奇才辈出,隐然成…

微软Build2021今日召开,共同期待VS2022+.NET6!

Microsoft Build 2021全球开发者大会将至,将带来什么惊喜呢?去年Build 2020是第一次完全线上举办的Build大会,是第一次完全属于开发者的大会,几乎所有的新产品都是属于开发者,开发者是唯一的主角!今年的Bui…

Google和百度都无法替代的10大深网搜索引擎

全世界只有3.14 % 的人关注了数据与算法之美当我们想要搜索某些内容时,我们第一个想到的就是打开Google、百度或必应这类的搜索引擎。但针对有些内容,却是这些常规搜索引擎无法获取到的,那就是隐藏在深网的内容。据不完全统计,深网…

编写properties文件的Eclipse插件

2019独角兽企业重金招聘Python工程师标准>>> 分享一个不错的编写properties文件的Eclipse插件(plugin),有了它我们在编辑一些简体中文、繁体中文等 Unicode文本时,就不必再使用native2ascii编码了。您可以通过Eclipse中…

php显示前60个字,DEDECMS中怎么让文章标题栏突破60个字符

DEDECMS中怎么让文章标题栏突破60个字符?1、使用PHPMYADMIN 修改 MYSQL数据结构CODE: ALTER TABLE dede_archives CHANGE title title VARCHAR( 250 ) [Copy to clipboard]2、打开/dede/action_article_save.php找到39行 CODE: $title cn_substr($title,60); [Copy…

数学是理工基础,如何才能令人信服?

随着科技的快速发展,人工智能的重要性日渐显现。而数学知识蕴含着处理智能问题的基本思想与方法,是理解复杂算法的必备要素。在机器学习工作流程中,数学与代码高度交织在一起,代码通常可以根据数学直观地构建,甚至会共…

Win7玩CF,不能全屏的解决方法...

今天用自己的本本玩CF,发天竟然不能全屏,抓狂呀! 在网上找了下,解决方法如下: 打开注册表,定位到: HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\GraphicsDrivers\Configuration\AUO183C0_01_07D9_17^773484D7596…

EFCore之增删改查

1. 连接数据库通过依赖注入配置应用程序&#xff0c;通过startup类的ConfigureService方法中的AddDbContext将EFCore添加到依赖注入容器public void ConfigureServices(IServiceCollection services) {services.AddControllers();services.AddDbContext<OpenDbContext>(o…

一堂儿童科学实验课引起的思考:数学和化学有什么关系?

全世界只有3.14 % 的人关注了数据与算法之美前段时间&#xff0c;我带侄子上了一堂化学课&#xff0c;回来之后&#xff0c;他一直意犹未尽找我的聊化学的事&#xff0c;期间他也问了身为数学专业的我一个交叉问题&#xff1a;叔&#xff0c;「数学」和「化学」有啥关系&#x…

使用 KubernetesClient 操作 kubernetes

使用 KubernetesClient 操作 kubernetesIntro我们的应用都是部署在 Kubernetes 上的&#xff0c;我们有一个服务内部有一层 MemoryCache&#xff0c;之前会依赖 Redis 的 Pub/Sub 来做缓存的更新&#xff0c;而 Redis 的 Pub/Sub 是一种不可靠的更新机制&#xff0c;容易发生消…

cent os重置mysql,linux mysql 能登陆不能修改用户(cent os 6.2)解决思路

linux mysql 能登陆不能修改用户(cent os 6.2)[root3mao /]# select user,host,password from mysql.userbash: syntax error near unexpected token from[root3mao /]# mysql -u rootWelcome to the MySQL monitor. Commands end with ; or /g.Your MySQL connection id is 4S…

本、硕、博到底有什么区别?清华教授的“兔子理论”让你快速弄懂

全世界只有3.14 % 的人关注了数据与算法之美前段时间&#xff0c;有人问到卢sir一个问题——“本、硕、博之间到底有什么区别&#xff1f;”曾经就有一位清华大学教授就讨论过这个问题&#xff0c;让我们来看看这位清华教授是如何看待本、硕、博区别的吧。作者 | 阎学通教授清华…