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

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

[[437425]]

一、背景

1. 讲故事

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

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

二、Windbg 分析

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

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

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

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

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

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

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

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

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

3. 寻找问题代码

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

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

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

  1. public async Task DistributionBundle(List<Bundle> list, List<xxx> bwdList, xxx item, List<xxx> sumDetails, List<xxx> details, BundleParameter bundleParameter, IEnumerable<dynamic> labels) 
  2.  int num = 0; 
  3.  foreach (xxx detail in sumDetails) 
  4.  { 
  5.   IEnumerable<xxx> woDetails = details.Where((xxx w) => w.Size == detail.Size && w.Color == detail.Color); 
  6.   foreach (xxx item2 in woDetails) 
  7.   { 
  8.             xxx 
  9.   } 
  10.   woDetails = woDetails.OrderBy((xxx s) => s.Seq).ToList(); 
  11.   num++; 
  12.         xxx 
  13.   Bundle bundle = new Bundle(); 
  14.   Bundle bundle2 = bundle; 
  15.   bundle2.BundleId = await _repo.CreateBundleId(); 
  16.    
  17.   foreach (xxx item3 in woDetails) 
  18.   { 
  19.    item3.TaskQty = item3.WoQty + Math.Ceiling(item3.WoQty * item3.OverCutRate); 
  20.    decimal value = default(decimal); 
  21.   } 
  22.  
  23.   await DistributionBundle(list, bwdList, item, sumDetails, details, bundleParameter, labels); 
  24.  } 

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

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

  1. 0:000> !DumpHeap /d -mt 00007ffdeb210098 
  2.          Address               MT     Size 
  3. 000001a297913a68 00007ffdeb210098      264      
  4. 000001a297913b70 00007ffdeb210098      264   
  5.  
  6. 0:000> !gcroot 000001a297913a68 
  7. Thread 5ac: 
  8.     000000470B1EE4E0 00007FFE45103552 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2922] 
  9.         rbp+10: 000000470b1ee550 
  10.             ->  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]] 
  11.             ->  000001A29796D8C0 Microsoft.Extensions.Hosting.Internal.Host 
  12.             ... 
  13.             ->  000001A298213248 System.Data.SqlClient.TdsParserStateObjectNative 
  14.             ->  000001A32E6AB700 System.Threading.Tasks.TaskFactory`1+<>c__DisplayClass38_0`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient],[System.Data.CommandBehavior, System.Data.Common]] 
  15.             ->  000001A32E6AB728 System.Threading.Tasks.Task`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]] 
  16.             ->  000001A32E6ABB18 System.Threading.Tasks.StandardTaskContinuation 
  17.             ->  000001A32E6ABA80 System.Threading.Tasks.ContinuationTaskFromResultTask`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]] 
  18.             ->  000001A32E6AB6C0 System.Action`1[[System.Threading.Tasks.Task`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]], System.Private.CoreLib]] 
  19.             ->  000001A32E6AB428 System.Data.SqlClient.SqlCommand+<>c__DisplayClass130_0 
  20.             ... 
  21.             ->  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]] 
  22.             ->  000001A32E6ABD20 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.String, System.Private.CoreLib],[xxx.DALs.xxx.BundleRepo+<CreateBundleId>d__12, xxx]] 
  23.             ->  000001A32E6ABD98 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  24.             ->  000001A32E6A6BD8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  25.             ->  000001A433250520 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  26.             ->  000001A32E69E0F8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  27.             ->  000001A433247D28 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  28.             ->  000001A433246330 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  29.             ->  000001A32E69A568 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  30.             ->  000001A433245408 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]] 
  31.             ... 

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

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

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

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

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

三、总结

本次内存泄漏的事故主要还是因为程序员的大意,也许是长期的 996 给弄恍惚了 ??????,有了这些信息,修正起来相信会非常简单。

本文转载自微信公众号「一线码农聊技术」,可以通过以下二维码关注。转载本文请联系一线码农聊技术公众号。

责任编辑:武晓燕 来源: 一线码农聊技术
相关推荐

2021-02-11 09:14:36

内存虚拟机数据

2012-05-18 00:01:07

JVMJavaJVM平台

2022-09-26 08:30:41

黑客网络攻击隐私

2009-03-10 12:42:45

2021-02-02 09:13:11

索引SQL数据库

2017-07-06 11:08:12

开源云计算EasyStack

2023-09-13 11:19:49

2023-09-13 17:08:31

2020-03-23 08:30:12

程序员男友感受

2011-03-25 15:35:55

ARM微软处理器架构

2016-07-20 17:19:21

SDN软件定义网络SDN商用

2018-03-21 18:00:15

NestJS

2021-12-15 10:20:08

缓存架构开发

2023-08-20 16:49:04

UbuntuLinux

2021-03-05 08:00:00

Web开发工具

2019-12-02 15:22:34

硬件 游戏显存

2020-05-26 08:52:36

Java JVM多态

2012-05-22 17:07:38

开发技术周刊

2019-12-03 13:16:55

PHP流封装协议
点赞
收藏

51CTO技术栈公众号