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

一:背景

1. 讲故事

去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了。。。截图如下:

9e9c0c53431f685ee2868f42045bfca1.png

时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了🤣🤣🤣

二:Windbg 分析

1. 托管还是非托管

这个是甄别内存问题的第一步,通过 !address -summary!eeheap -gc 两个命令基本就可以断定。

0:000> !address -summary                             
Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                    237     7ffc`1e222000 ( 127.985 TB)           99.99%
<unknown>                               594        3`b9b20000 (  14.901 GB)  95.96%    0.01%
Heap                                    370        0`12a2a000 ( 298.164 MB)   1.88%    0.00%
Image                                  1248        0`0ee5a000 ( 238.352 MB)   1.50%    0.00%
Stack                                   315        0`06780000 ( 103.500 MB)   0.65%    0.00%
Other                                    13        0`001d7000 (   1.840 MB)   0.01%    0.00%
TEB                                     105        0`000d2000 ( 840.000 kB)   0.01%    0.00%
PEB                                       1        0`00001000 (   4.000 kB)   0.00%    0.00%--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                            1178        3`ce03d000 (  15.219 GB)  98.00%    0.01%
MEM_IMAGE                              1409        0`0f6fd000 ( 246.988 MB)   1.55%    0.00%
MEM_MAPPED                               59        0`04694000 (  70.578 MB)   0.44%    0.00%--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                237     7ffc`1e222000 ( 127.985 TB)           99.99%
MEM_COMMIT                             2326        3`c7543000 (  15.115 GB)  97.33%    0.01%
MEM_RESERVE                             320        0`1a88b000 ( 424.543 MB)   2.67%    0.00%0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000009902B57670
generation 1 starts at 0x0000009902A56810
generation 2 starts at 0x00000095318C1000
ephemeral segment allocation context: (0x0000009902D724A8, 0x0000009902D724C0)segment             begin         allocated         committed    allocated size    committed size
...
00000098FFBE0000  00000098FFBE1000  0000009902D724A8  0000009902D7D000  0x31914a8(51975336)  0x319c000(52019200)
Large object heap starts at 0x00000095418C1000segment             begin         allocated         committed    allocated size    committed size
00000095418C0000  00000095418C1000  00000095475D8D98  00000095475D9000  0x5d17d98(97615256)  0x5d18000(97615872)
Total Allocated Size:              Size: 0x398e6cbc8 (15450164168) bytes.
Total Committed Size:              Size: 0x398e7b000 (15450222592) bytes.
------------------------------
GC Allocated Heap Size:    Size: 0x398e6cbc8 (15450164168) bytes.
GC Committed Heap Size:    Size: 0x398e7b000 (15450222592) bytes.

从输出信息看,好家伙。。。进程提交内存是 15G, 托管堆差不多也是 15G,这就说明当前是相对简单的 托管内存泄漏

2. 到底是什么在泄漏

要想知道到底是什么在泄漏,可以先到托管堆上看看有没有什么异常的对象,使用 !dumpheap -stat 命令。

0:000> !dumpheap -stat
Statistics:MT    Count    TotalSize Class Name
...
00007ff8815d0f88  7260233    290409320 System.Collections.ArrayList
00007ff8815e6830  7313696    326240826 System.String
000000952fbbd2b0 12141398    509369998      Free
00007ff880685cf0  7254983    928637824 System.Diagnostics.ProcessInfo
00007ff88065f7d0  7256845   2031916600 System.Diagnostics.Process
00007ff8815e6ea8  7391338   2230744600 System.Object[]
00007ff88068fa70 186800748   8966435904 System.Diagnostics.ThreadInfo

从卦象上来看,真的太奇怪了,如果大家了解 Process 类,应该知道 ProcessInfoThreadInfo 都是挂在 Process 下的,而且 ThreadInfo 对象高达 1.8亿,真的太🐂👃了,看样子程序是在不断的做 Process.Start 操作吧。

接下来要探究的问题是 ThreadInfo 到底正在被谁持有???可以挑几个看看它们的 !gcroot, 首尾法查了几个,都是没有引用根,如下所示:

0:000> !gcroot 0000009531e8f760
Found 0 unique roots (run '!GCRoot -all' to see all roots).
0:000> !gcroot 0000009531e8f670 
Found 0 unique roots (run '!GCRoot -all' to see all roots).
0:000> !gcroot 0000009531e8f378
Found 0 unique roots (run '!GCRoot -all' to see all roots).

3. 无引用根为什么不被回收

既然对象没有引用根,为什么 GC 不回收它呢?这里就需要谈经验了,在我之前分析的很多关于内存泄漏 的dump中,我都是从 生产端 找问题,貌似还没有出现一个从 消费端 找问题的案例,参考模型如下:

8b7d01660257e9d5b19ad8cbf7232c73.png

生产者既然没问题,那消费端能有什么问题呢?大家可以想一想,托管堆的消费端起码有两个角色。

  1. GC

  2. Finalizer 线程

GC 肯定是不会出问题的,那就只能怀疑 Finalizer 线程出了什么问题,可以用  !t 命令把所有线程调出来看看。

0:000> !t
ThreadCount:      9566
UnstartedThread:  0
BackgroundThread: 88
PendingThread:    0
DeadThread:       9471
Hosted Runtime:   noLock  ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception0    1 40e18 000000952fbdfe50    26020 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 2    2 41ce8 000000952fc0e4d0    2b220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Finalizer) 4    4 41cb4 000000954c324970    27220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 5    5 41cb8 000000954c36d1e0  2027220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 6    6 41c58 000000954c33f070  2027220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 7    7 41c38 000000954c33f840    27220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 8    8 41e0c 000000954c333580    27220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 9    9 41e2c 000000954c354440    27220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 10   10 41f24 000000954c355840    27220 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     STA 
...
XXXX 9446    0 000000957a233410  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port) 
XXXX 9447    0 0000009579f83e30  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port) 
XXXX 9450    0 000000957a46dcf0  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port) 
XXXX 9449    0 000000957967c6e0  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port) 
XXXX 9448    0 000000957aee0010  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port) 
XXXX 9452    0 00000095796824a0  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port) 
XXXX 9451    0 000000957af05df0  8039820 Preemptive  0000000000000000:0000000000000000 000000952fbd2e40 0     MTA (Threadpool Completion Port)

这不看不知道,一看吓一跳,当前进程有 9566 个,死线程高达 9471 个 ,以过往经验,这小子不用线程池,用 new Thread 咯, 🐂👃🦆 ,吐槽结束,再看下 Finalizer 线程正在做什么,使用 ~2s & !dumpstack

0:002> ~2s
ntdll!NtWaitForSingleObject+0xa:
00007ff8`8e220c8a c3              ret
0:002> !dumpstack
OS Thread Id: 0x41ce8 (2)
Current frame: ntdll!NtWaitForSingleObject+0xa
Child-SP         RetAddr          Caller, Callee
0000009549e4e120 00007ff88b591118 KERNELBASE!WaitForSingleObjectEx+0x94, calling ntdll!NtWaitForSingleObject
0000009549e4e1c0 00007ff88da3e334 combase!MTAThreadWaitForCall+0x54 [d:\9147\com\combase\dcomrem\channelb.cxx:5657], calling KERNELBASE!WaitForSingleObject
0000009549e4e210 00007ff88d8fe089 combase!MTAThreadDispatchCrossApartmentCall+0x75 [d:\9147\com\combase\dcomrem\chancont.cxx:193], calling combase!MTAThreadWaitForCall [d:\9147\com\combase\dcomrem\channelb.cxx:5619]
0000009549e4e240 00007ff88da3e13d combase!CRpcChannelBuffer::SendReceive2+0x64b [d:\9147\com\combase\dcomrem\channelb.cxx:4796], calling combase!MTAThreadDispatchCrossApartmentCall [d:\9147\com\combase\dcomrem\chancont.cxx:156]
0000009549e4e2b0 00007ff88e1bb6f7 ntdll!RtlAllocateHeap+0xd7, calling ntdll!RtlpLowFragHeapAllocFromContext
...
0000009549e4f5d0 00007ff8827d79cd clr!ManagedThreadBase_DispatchOuter+0x75, calling clr!ManagedThreadBase_DispatchMiddle
0000009549e4f5e0 00007ff8828601af clr!EEConfig::GetConfigDWORD_DontUse_+0x3b, calling clr!EEConfig::GetConfiguration_DontUse_
0000009549e4f660 00007ff8828574fa clr!FinalizerThread::FinalizerThreadStart+0x10a, calling clr!ManagedThreadBase_DispatchOuter
0000009549e4f6a0 00007ff8827d55b9 clr!EEHeapFreeInProcessHeap+0x45, calling kernel32!HeapFreeStub
0000009549e4f700 00007ff882882e8f clr!Thread::intermediateThreadProc+0x86
0000009549e4f780 00007ff882882e6f clr!Thread::intermediateThreadProc+0x66, calling clr!_chkstk
0000009549e4f7c0 00007ff88dcd13d2 kernel32!BaseThreadInitThunk+0x22
0000009549e4f7f0 00007ff88e2003c4 ntdll!RtlUserThreadStart+0x34

从堆栈信息看,原来是终结器线程卡死了,从 MTAThreadDispatchCrossApartmentCall 方法看,貌似是 MTASTA 做了一个调用,到这里有经验的朋友应该知道,这和 com 组件有关系了,也就是说 Finalizer 线程无法释放由 STA 线程创建的 COM 对象,那到底是哪一个线程创建了没有被合理释放的 COM 组件呢?

4. 寻找创建 COM 组件的线程

说实话,这个对COM组件不了解的话,很难找出答案,但天无绝人之路,当我回头看 线程列表 的时候,发现居然有 38 个 STA线程,截图如下:

31b69fa6601101f6293067402514f00c.png

这里面肯定有问题,接下来抽一个线程看看调用栈如何。

0:004> !clrstack 
OS Thread Id: 0x41cb4 (4)Child SP               IP Call Site
000000954da1ee38 00007ff88e220c8a [HelperMethodFrame: 000000954da1ee38] System.Threading.Thread.SleepInternal(Int32)
000000954da1ef30 00007ff88138c20a *** WARNING: Unable to verify checksum for mscorlib.ni.dll
System.Threading.Thread.Sleep(Int32)
000000954da1ef60 00007ff82322437f xxx.CFileLogTask.DoWork()
000000954da1f160 00007ff8232234d6 xxx.CTask.InitStart()
000000954da1f240 00007ff8813c31d3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000954da1f310 00007ff8813c3064 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000954da1f340 00007ff8813c3032 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
000000954da1f390 00007ff8813bc812 System.Threading.ThreadHelper.ThreadStart()
000000954da1f5e8 00007ff8827d6bb3 [GCFrame: 000000954da1f5e8] 
000000954da1f938 00007ff8827d6bb3 [DebuggerU2MCatchHandlerFrame: 000000954da1f938]

接下来反编译下 xxx.CFileLogTask.DoWork() 方法看看它是如何被 Thread 执行的。

f037277a4c619f5f2d9c0c4222ba9f38.png

到这里终于水落石出,罪魁祸首在 CurrThread.SetApartmentState(ApartmentState.STA); 这一句上,我也不知道为啥开个 Thread 还要给个 STA。。。

三:总结

本次事故主要是因为在 STA 线程上用到了 COM 组件,导致让 MTA 模型的 Finalizer 线程去释放时被卡死,而这个Thread又没有用 Application.Run 启动消息循环,STA也是 Sleep 状态,我个人感觉两者无法通讯,给到朋友的建议是去掉 Thread 的 STA。

其实这里有一个很好的点就是,当内存暴涨,不一定是 生产端 的问题,也有可能是 消费端

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

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

相关文章

高性能网站建设的最佳实践(二)

原文译自雅虎开发者社区&#xff0c;转载译文请标明出处。关注我的sina微博&#xff0c;共同进步&#xff01;为了让网页响应速度更快Exceptional Performance团队列出了一系列的最佳实践&#xff0c;包括35个最佳实践条目&#xff0c;分成7种类型类。避免重定向标签&#xff1…

python带通配符的字符串匹配_Bash技巧:实例介绍数个参数扩展表达式以处理字符串变量...

Linux 的 bash shell 提供了多种形式的参数扩展表达式&#xff0c;可以获取变量自身的值&#xff0c;或者对变量值进行特定处理得到一个新的值&#xff0c;等等。本篇文章对字符串变量值相关的参数扩展表达式进行汇总说明。假设在 bash 中定义了 filepathexample/subdir/testfi…

幸福手机,给爸妈的高端大气上档次的手机

打造高端老人手机——幸福手机 江苏智联天地科技有限公司历经2年&#xff0c;手机研发投入超过4000万&#xff0c;打造中国第一品牌的高端老人手机&#xff0c;手机将于2014年12月正式对外发布&#xff0c;是国内第一款高端老人手机——幸福手机&#xff08;ThimFone&#xff0…

MASA Framework - EventBus设计

概述利用发布订阅模式来解耦不同架构层级&#xff0c;亦可用于解决隔离业务之间的交互优点&#xff1a;松耦合横切关注点可测试性事件驱动发布订阅模式发布者通过调度中心将消息发送给订阅者。调度中心解决发布与订阅者之间的关系&#xff0c;保证消息可以送达订阅者手中。发布…

wireshark-win64-3.4.0安装_这9类轴承的安装方法,你可都知道?有哪些需要注意的呢?...

轴承是当代机械设备中一种重要零部件。随着时间的推移&#xff0c;轴承会发生磨损&#xff0c;合理的安装和使用可以让机械设备减少不必要的安全隐患。前面文章讲了如何拆卸轴承&#xff0c;今天就给大家讲讲各类轴承应该如何安装&#xff01;一、轴承安装前的准备工作轴承的安…

史上最牛数学简史

全世界只有3.14 % 的人关注了爆炸吧知识“中国现代数学之父”华罗庚曾说过宇宙之大&#xff0c;粒子之微火箭之速&#xff0c;化工之巧地球之变&#xff0c;生物之谜日用之繁&#xff0c;无处不用数学回首往昔数学始终伴随我们左右纵横交错的几何、繁琐复杂的运算难以求解的方程…

linux下使用pidcat找bug

第一步&#xff1a; 安装pidcat 第二步&#xff1a; 找到APP的包名比如adb shell ps | grep sangforadb shell pm list package第三步&#xff1a; 在ubuntu终端输入pidcat.py 包名结果&#xff1a;

创建与删除索引

索引是加速查询的主要手段&#xff0c;特别对于涉及多个表的查询更是如此。本节中&#xff0c;将介绍索引的作用、特点&#xff0c;以及创建和删除索引的语法。13.4.1 使用索引优化查询索引是高速定位数据的技术&#xff0c;首先通过一个演示样例来了解其含义及作用&#xff0…

r vector 4 elements_Vector类与Enumeration接口

Vector类用于保存一组对象&#xff0c;由于java不支持动态数组&#xff0c;Vector可以用于实现跟动态数组差不多的功能。如果要将一组对象存放在某种数据结构中&#xff0c;但是不能确定对象的个数时&#xff0c;Vector是一个不错的选择。例&#xff1a;将键盘上输入的一个数字…

[深入JUnit] 测试运行的入口

阅读前提 了解JUnit 对JUnit的内部实现有兴趣 不妨看看[深入JUnit] Before, After, Test的秘密] 代码版本: junit 4.12代码搜索工具&#xff1a; http://grepcode.com/常用符号 _: 用来略去代码段中无关紧要的parameter ...: 用来略去无关紧要的代码实现 本文的展开方式&…

.NET6之MiniAPI(七):中间件

http协议&#xff0c;是由客户端发出请求&#xff0c;服务端响应结果并返回&#xff0c;我们把这个请求来回抽象成一个请求管道&#xff0c;那中间件就是这个管道上的阀门&#xff0c;控制着流量的进出和中断。每一个请求都要经过中间件的过滤&#xff0c;滤掉不合格的请求&…

linux之telnet命令使用

telnet命令通常用来远程登录。telnet程序是基于TELNET协议的远程登录客户端程序。Telnet协议是TCP/IP协议族中的一员&#xff0c;是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的 能力。在终端使用者的电脑上使用telnet程序&…

arraylist从大到小排序_经典排序方法的python实现和复杂度分析

1.冒泡排序:冒泡排序算法的运作如下&#xff1a;比较相邻的元素。如果第一个比第二个大(升序)&#xff0c;就交换他们两个。对每一对相邻元素作同样的工作&#xff0c;从开始第一对到结尾的最后一对。这步做完后&#xff0c;最后的元素会是最大的数。针对所有的元素重复以上的步…

HOOK学习笔记与心得

一、 Hook介绍钩子(Hook)&#xff0c;是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息&#xff0c;而且所监视的窗口可以是其他进程所创建的。当消息到达后&#xff0c;在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理wind…

access函数_ACCESS中的DLookUp函数是如何运算的?

​一、DLookUp函数介绍1. DLookUp函数的用途&#xff1a;可以用于从指定集合(一个域)中获取符合条件的特定字段的值。2. DLookUp函数的格式为&#xff1a;DLookUp( expr , domain , [criteria] )其中&#xff1a;expr 为字段名&#xff0c;或以字段名为基础的表达式字符串domai…

汇编语言之基础知识

1、机器语言 说到汇编语言的产生&#xff0c;首先要讲一下机器语言。机器语言是机器指令的集合。什么是机器指令&#xff1f;我们在使用CE时&#xff0c;常常见到。 请看下图&#xff1a; 图中所示的就是机器指令&#xff08;或称机器码&#xff09;&#xff0c;这是十六进制的…