一:背景

1. 讲故事

上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下:

朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧。

二:Windbg 分析

1. 到底是哪一方面的泄漏

根据朋友描述,程序运行一段时间后,内存就炸了,应该没造成人员伤亡,不然也不会跟我wx聊天了,这里可以用 .time 看看当前的 process 跑了多久。

0:000> .time
Debug session time: Thu Oct 21 14:54:39.000 2021 (UTC + 8:00)
System Uptime: 6 days 4:37:27.851
Process Uptime: 0 days 0:40:14.000Kernel time: 0 days 0:01:55.000User time: 0 days 0:07:33.000

看的出来,这个 dump 是在程序跑了 40min 之后抓的,接下来我们比较一下 process 的内存和 gc堆 占比, 看看到底是哪一块的泄漏。

0:000> !address -summary--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                327     7dfc`c665a000 ( 125.987 TB)           98.43%
MEM_RESERVE                             481      201`e91a2000 (   2.007 TB)  99.74%    1.57%
MEM_COMMIT                             2307        1`507f4000 (   5.258 GB)   0.26%    0.00%0:000> !eeheap -gc
Number of GC Heaps: 2
------------------------------GC Allocated Heap Size:    Size: 0x139923528 (5260850472) bytes.
GC Committed Heap Size:    Size: 0x13bf23000 (5300695040) bytes.

从卦中可轻松得知, 这是一个完完全全的托管堆内存泄漏。

2. 到底是什么占用了如此大的内存

知道是 托管层 的泄漏,感觉一下子就幸福起来了,接下来用 !dumpheap -stat 看看有没有什么大对象可挖。

0:000> !dumpheap -stat
Statistics:MT    Count    TotalSize Class Name
00007ffdeb1fc400  5362921    128710104 xxxBLLs.xxx.BundleBiz+<>c__DisplayClass20_0
00007ffdeaeff140  5362929    171613728 System.Collections.Generic.List`1[[xxx.xxx, xxx]]
00007ffdeaeff640  5362957    171615272 xxx.BLLs.Plan.Dto.xxx[]
00007ffde8171e18 16146362    841456072 System.String
00007ffdeb210098  5362921   1415811144 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
00007ffdea9ca260  5362921   2359685240 xxx.Bundle

从输出看,内存主要被 xxx.BundleAsyncTaskMethodBuilder 两大类对象给吃掉了,数量都高达 536w,这里有一个非常有意思的地方,如果你了解异步,我相信你一看就能看出 AsyncTaskMethodBuilder + VoidTaskResult 是干嘛的,按照经验,这位朋友应该误入了 异步无限递归 ,那怎么去挖呢?接着往下看。

3. 寻找问题代码

看到上面的 xxx.BundleBiz+<DistributionBundle>d__20 了吗?这个正是异步操作所涉及到的类和方法,接下来用 ILSpy 反射出 BundleBiz 下的匿名类 <DistributionBundle>d__20 , 如下图所示:

虽然找到了源码,但代码是 ILSpy 反编译出来的异步状态机,接下来的一个问题是,如何根据状态机代码反向寻找到 await ,async 代码呢?在 ILSpy 中有一个 used by 功能,在这里可以用起来了。

双击 used by 就能看到真正的调用代码,简化后如下:

public async Task DistributionBundle(List<Bundle> list, List<xxx> bwdList, xxx item, List<xxx> sumDetails, List<xxx> details, BundleParameter bundleParameter, IEnumerable<dynamic> labels)
{int num = 0;foreach (xxx detail in sumDetails){IEnumerable<xxx> woDetails = details.Where((xxx w) => w.Size == detail.Size && w.Color == detail.Color);foreach (xxx item2 in woDetails){xxx}woDetails = woDetails.OrderBy((xxx s) => s.Seq).ToList();num++;xxxBundle bundle = new Bundle();Bundle bundle2 = bundle;bundle2.BundleId = await _repo.CreateBundleId();foreach (xxx item3 in woDetails){item3.TaskQty = item3.WoQty + Math.Ceiling(item3.WoQty * item3.OverCutRate);decimal value = default(decimal);}await DistributionBundle(list, bwdList, item, sumDetails, details, bundleParameter, labels);}
}

仔细看上面这段代码, 我去, await DistributionBundle(list, bwdList, item, sumDetails, details, bundleParameter, labels); 又调用了自身,看样子是某种条件下陷入了一个死递归。。。

有些朋友可能要问,除了经验之外,能从 dump 中分析出来吗?当然可以,从 500w+ 中抽一个看看它的 !gcroot 即可。

0:000> !DumpHeap /d -mt 00007ffdeb210098Address               MT     Size
000001a297913a68 00007ffdeb210098      264
000001a297913b70 00007ffdeb210098      264  0:000> !gcroot 000001a297913a68
Thread 5ac:000000470B1EE4E0 00007FFE45103552 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2922]rbp+10: 000000470b1ee550->  000001A297A25D88 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions+<RunAsync>d__4, Microsoft.Extensions.Hosting.Abstractions]]->  000001A29796D8C0 Microsoft.Extensions.Hosting.Internal.Host...->  000001A298213248 System.Data.SqlClient.TdsParserStateObjectNative->  000001A32E6AB700 System.Threading.Tasks.TaskFactory`1+<>c__DisplayClass38_0`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient],[System.Data.CommandBehavior, System.Data.Common]]->  000001A32E6AB728 System.Threading.Tasks.Task`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]]->  000001A32E6ABB18 System.Threading.Tasks.StandardTaskContinuation->  000001A32E6ABA80 System.Threading.Tasks.ContinuationTaskFromResultTask`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]]->  000001A32E6AB6C0 System.Action`1[[System.Threading.Tasks.Task`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]], System.Private.CoreLib]]->  000001A32E6AB428 System.Data.SqlClient.SqlCommand+<>c__DisplayClass130_0...->  000001A32E6ABC08 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.String, System.Private.CoreLib],[Dapper.SqlMapper+<QueryRowAsync>d__34`1[[System.String, System.Private.CoreLib]], Dapper]]->  000001A32E6ABD20 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.String, System.Private.CoreLib],[xxx.DALs.xxx.BundleRepo+<CreateBundleId>d__12, xxx]]->  000001A32E6ABD98 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A32E6A6BD8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A433250520 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A32E69E0F8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A433247D28 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A433246330 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A32E69A568 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]->  000001A433245408 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]...

从调用栈来看,代码貌似是从数据库读取记录的过程中陷入死循环的。

4. 为什么没有出现栈溢出

一看到无限循环,我相信很多朋友肯定要问,为啥没出现堆栈溢出,毕竟默认的线程栈空间仅仅 1M 而已,从 !gcroot 上看,这些引用都是挂在 5ac 线程上,也就是下面输出的 主线程 ,而且主线程栈也非常干净。

0:000> !t
ThreadCount:      30
UnstartedThread:  0
BackgroundThread: 24
PendingThread:    0
DeadThread:       5
Hosted Runtime:   noLock  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception0    1      5ac 000001A29752CDF0  202a020 Preemptive  0000000000000000:0000000000000000 000001a29754c570 0     MTA 4    2     1e64 000001A29752A490    2b220 Preemptive  0000000000000000:0000000000000000 000001a29754c570 0     MTA (Finalizer)
...0:000> !clrstack
OS Thread Id: 0x5ac (0)Child SP               IP Call Site
000000470B1EE1D0 00007ffe5eb30544 [GCFrame: 000000470b1ee1d0]
000000470B1EE318 00007ffe5eb30544 [HelperMethodFrame_1OBJ: 000000470b1ee318] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
000000470B1EE440 00007ffe45103c25 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
000000470B1EE4E0 00007ffe45103552 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2922]
000000470B1EE550 00007ffe451032cf System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2861]
000000470B1EE5D0 00007ffe45121b04 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) [/_/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/TaskAwaiter.cs @ 143]
000000470B1EE600 00007ffe4510482d System.Runtime.CompilerServices.TaskAwaiter.GetResult() [/_/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/TaskAwaiter.cs @ 106]
000000470B1EE630 00007ffe4de36595 Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(Microsoft.Extensions.Hosting.IHost) [/_/src/Hosting/Abstractions/src/HostingAbstractionsHostExtensions.cs @ 49]
000000470B1EE660 00007ffde80f3b4b xxx.Program.Main(System.String[])
000000470B1EE8B8 00007ffe47c06c93 [GCFrame: 000000470b1ee8b8]
000000470B1EEE50 00007ffe47c06c93 [GCFrame: 000000470b1eee50]

如果你稍微了解一点异步的玩法,你应该知道这其中有一个 IO完成端口 的概念,它可以实现 句柄ThreadPool 的绑定,无限递归只不过是进了 IO完成端口 的待回调队列中而已,理论上和栈空间没什么关系,也就不会出现栈溢出了。

三:总结

本次内存泄漏的事故主要还是因为程序员的大意,也许是长期的 996 给弄恍惚了

程序内存一直在泄漏,原来是异步死循环了 !相关推荐

  1. C++ 程序内存泄漏检测方法

    一.前言 在Linux平台上有valgrind可以非常方便的帮助我们定位内存泄漏,因为Linux在开发领域的使用场景大多是跑服务器,再加上它的开源属性,相对而言,处理问题容易形成"统一&qu ...

  2. valgrind 内存泄漏_应用 AddressSanitizer 发现程序内存错误

    应用 AddressSanitizer 发现程序内存错误 作为 C/ C++ 工程师,在开发过程中会遇到各类问题,最常见便是内存使用问题,比如,越界,泄漏.过去常用的工具是 Valgrind,但使用 ...

  3. Linux 下几款程序内存泄漏检查工具

    Linux 下几款程序内存泄漏检查工具 chenyoubing | 发布于 2016-07-23 10:08:09 | 阅读量 93 | 无 写这篇博客的原因呢是因为自己在编写基于Nginx磁盘缓存管 ...

  4. linux c 代码分析工具,编程达人 分享几款Linux 下C/C++程序内存泄漏检查工具

    1.内存管理是否正确(因为这个程序本身开辟很多内存空间进行缓存管理,同时这个程序程序本身就是基于C/C++开发的,内存管理机制一直是程序员头痛的东西) 2.程序的健硕性如何(服务器任何程序的基本要求就 ...

  5. java程序内存泄漏场景及预防

    为什么80%的码农都做不了架构师?>>>    虽然jvm有垃圾回收机制,如果程序编写不注意某些特定规则,仍然会导致java程序内存泄漏,最终可能出现OutOfMemory异常. 1 ...

  6. go每日新闻(2021-08-29)——Go程序内存假泄漏是怎么回事

    每日一谚:Your Go functions should not have six return values. . go中文网每日资讯--2021-08-29 一.Go语言中文网 IEEE 202 ...

  7. Windows程序内存泄漏(Memory Leak)分析之UMDH

    小木发现线上的程序通过任务管理器发现内存不断的增长,怀疑是不是内存泄漏呢?用户态内存泄漏可能是句柄泄漏,堆内存泄露,Socket, GDI对象等等.而对于C++程序员来说,碰到最多的无疑是堆内存泄露: ...

  8. 关于Android应用程序内存泄漏 你需要知道的一切

    关于Android应用程序内存泄漏 你需要知道的一切 原文:https://blog.aritraroy.in/everything-you-need-to-know-about-memory-lea ...

  9. android应用内存分析,Android应用程序内存分析-Memory Analysis for Android Applications

    Android应用程序内存分析 原文链接:http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html ...

最新文章

  1. android webview开启html5支持
  2. 『数据中心』降低PUE值4种方法
  3. 微信 登录 Scope 参数错误或没有 Scope 权限
  4. dbgrid的最小高度设置。否则出现滚动条。
  5. 并发编程之 Semaphore 源码分析
  6. Docker-Desktop储存路径更改
  7. Modbus协议栈开发笔记之一:实现功能的基本设计
  8. 从前端到后台,开发一个完整功能的小程序
  9. ASP.NET ZERO 学习 JTable的使用
  10. paip.php页面调试设置及流程
  11. 数字电子技术基础 数电 第六版 课后答案(全)
  12. 小米9pro计算机打不开,小米9pro怎么连接电脑
  13. 【PAT_1054】The Dominant Color
  14. 与君初相识之Linux与Java SE
  15. 微信小程序实现多语言方案|中英互译
  16. 微积分review 极限,迫敛性,极限四则运算,自然常数来历
  17. 春招必看一位老学长的真实互联网校招求职心路历程~
  18. 自定义Navigator切换fragment
  19. 30 分钟 HTTP 查漏补缺之 Vary
  20. 锁定Mac电脑的8种方法

热门文章

  1. @action 注解
  2. 回忆一 --- 去年6月面试进入公司的日子
  3. 新浪微博、腾讯微博、QQ空间、人人网、豆瓣 一键分享API
  4. Apache伪静态学习
  5. (24) 不可能的出栈顺序
  6. 如何下载EP的各个版本?
  7. EVGA Precision—— 显卡超频神器 可用于调节风扇转速 降温
  8. USING HAVING
  9. Office SharePoint Server 2007
  10. 【年度总结】2016年年度总结