记一次 .NET某管理局检测体系 内存暴涨分析

打印 上一主题 下一主题

主题 814|帖子 814|积分 2442

一:背景

1. 讲故事

前些天有位朋侪微信找到我,说他们的WPF程序有内存走漏的环境,让我帮忙看下怎么回事?并且dump也抓到了,网上关于程序内存走漏,内存暴涨的文章不计其数,看样子这个dump不是很好分析,不管怎么说,上 windbg 说话。
二:WinDbg分析

1. 内存真的暴涨吗

在.NET调试训练营中我一直强调要相信数据,不要相信别人的一面之词,往往会把你带到沟里去,接下来利用 !address -summary 观察下提交内存。
  1. 0:000> !address -summary
  2. --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  3. Free                                    586     7dfd`f04e3000 ( 125.992 TB)           98.43%
  4. <unknown>                              1390      201`5a9bc000 (   2.005 TB)  99.86%    1.57%
  5. Heap                                   3989        0`7695c000 (   1.853 GB)   0.09%    0.00%
  6. Image                                  1744        0`2077d000 ( 519.488 MB)   0.02%    0.00%
  7. Stack                                   957        0`1dc00000 ( 476.000 MB)   0.02%    0.00%
  8. TEB                                     319        0`0027e000 (   2.492 MB)   0.00%    0.00%
  9. Other                                    61        0`001f9000 (   1.973 MB)   0.00%    0.00%
  10. PEB                                       1        0`00001000 (   4.000 kB)   0.00%    0.00%
  11. ...
  12. --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
  13. MEM_FREE                                586     7dfd`f04e3000 ( 125.992 TB)           98.43%
  14. MEM_RESERVE                            2028      201`46def000 (   2.005 TB)  99.85%    1.57%
  15. MEM_COMMIT                             6433        0`c8d1e000 (   3.138 GB)   0.15%    0.00%
  16. ...
复制代码
从卦中可知当前的提交内存是 3.1G,对于一个窗体程序来说这个内存量算是比较大了,接下来利用 !eeheap -gc 观察下托管堆内存。
  1. 0:000> !eeheap -gc
  2. ========================================
  3. Number of GC Heaps: 1
  4. ----------------------------------------
  5. generation 0 starts at 1b368e4de10
  6. generation 1 starts at 1b3687ea4f0
  7. generation 2 starts at 1b300001000
  8. ephemeral segment allocation context: none
  9. Small object heap
  10.          segment            begin        allocated        committed allocated size         committed size        
  11.     01b300000000     01b300001000     01b30fffff88     01b310000000 0xfffef88 (268431240)  0x10000000 (268435456)
  12.     01b35dc70000     01b35dc71000     01b368e8fe28     01b369995000 0xb21ee28 (186773032)  0xbd25000 (198332416)
  13. Large object heap starts at 1b310001000
  14.          segment            begin        allocated        committed allocated size         committed size        
  15.     01b310000000     01b310001000     01b316d40560     01b316d41000 0x6d3f560 (114554208)  0x6d41000 (114561024)
  16.     01b3cfc50000     01b3cfc51000     01b3d6588320     01b3d6589000 0x6937320 (110326560)  0x6939000 (110333952)
  17. Pinned object heap starts at 1b318001000
  18.          segment            begin        allocated        committed allocated size         committed size        
  19.     01b318000000     01b318001000     01b3180812d0     01b318082000 0x802d0 (525008)       0x82000 (532480)      
  20. ------------------------------
  21. GC Allocated Heap Size:    Size: 0x28914900 (680610048) bytes.
  22. GC Committed Heap Size:    Size: 0x29421000 (692195328) bytes.
复制代码
从卦中数据看,当前的托管堆也才 692M,和当前的 3G 相差甚远,这就阐明这个程序出现了比较麻烦的 非托管内存走漏,接下来转头看下内存地址段发现 Heap=1.8G ,有了这个数据后用 !heap -s 观察下地址段。
  1. 0:000> !heap -s
  2. ************************************************************************************************************************
  3.                                               NT HEAP STATS BELOW
  4. ************************************************************************************************************************
  5. LFH Key                   : 0x3861e2c156213079
  6. Termination on corruption : ENABLED
  7.           Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
  8.                             (k)     (k)    (k)     (k) length      blocks cont. heap
  9. -------------------------------------------------------------------------------------
  10. 000001b37a6b0000 00000002  194824 183768 194432  29846  1716    20   30   6fa1   LFH
  11.     External fragmentation  16 % (1716 free blocks)
  12. 000001b37a4e0000 00008000      64      8     64      6     1     1    0      0      
  13. 000001b37c140000 00001002    3516   2492   3124    476    69     3    0      0   LFH
  14.     External fragmentation  19 % (69 free blocks)
  15. 000001b37c380000 00001002      60     36     60      8     3     1    0      0      
  16. 000001b37c360000 00041002      60      8     60      5     1     1    0      0      
  17. 000001b37d510000 00001002    1472     88   1080     38     7     2    0      0   LFH
  18. 000001b320a10000 00001002    1472    204   1080     71    12     2    0      0   LFH
  19. 000001b327a60000 00001002     452     32     60      4     3     1    0      0   LFH
  20. 000001b3292b0000 00001002 1513284 1215876 1512892  74984  6445   924    4 2e72c3   LFH
  21.     Virtual address fragmentation  19 % (924 uncommited ranges)
  22.     Lock contention  3044035
  23. 000001b327e80000 00001002    1472    812   1080    439    11     2    0      2   LFH
  24. 000001b327cb0000 00001002    3516   1140   3124    519    12     3    0      0   LFH
  25.     External fragmentation  45 % (12 free blocks)
  26. 000001b327ec0000 00001002    1472    824   1080    468    10     2    0      0   LFH
  27. 000001b327cc0000 00001002    1472   1012   1080    441    11     2    0      0   LFH
  28. -------------------------------------------------------------------------------------
复制代码
从卦中数据看当前的内存都被 Heap=000001b3292b0000 这个私有heap给吃掉了,看样子是某个程序为了某个目的单独分配的,由于没有开启 ust ,这里就没法举行下去了,接下来陷入了迷茫。
2. 在绝望中寻找希望

没有开启ust是不是就没有突破口了呢?大多环境下是的,但作为调试师,必要具备在 绝望中寻找希望 的能力,再转头看地址段,发现 TEB=319,也就说当前程序有 319 个线程,对于一个窗体程序来说这么多线程很显着是一个异常信号,那这个就是突破口,先用 !tp 观察下托管线程列表。

从卦中数据看基本都是线程池的工作线程,为什么会开启这么多线程呢?第一个反应就是线程是不是卡住了?立即用 !syncblk 命令做下验证。
  1. 0:000> !syncblk
  2. Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
  3. 2363 000001B3984D6928          381         1 000001B335581A80 607c 135   000001b35e3a0d98 System.Object
  4. -----------------------------
  5. Total           2410
  6. CCW             301
  7. RCW             126
  8. ComClassFactory 1
  9. Free            1783
复制代码
我去。。。卦中的数据又让我看到了希望!原来有190 个线程卡在 System.Object 锁上,赶紧找个线程观察下线程栈,为了隐私我就多潜伏一点。
  1. 0:263> ~~[5a2c]s
  2. ntdll!NtWaitForMultipleObjects+0x14:
  3. 00007fff`c800fec4 c3              ret
  4. 0:292> !clrstack
  5. OS Thread Id: 0x5a2c (292)
  6.         Child SP               IP Call Site
  7. 0000002E98DFEB48 00007fffc800fec4 [HelperMethodFrame_1OBJ: 0000002e98dfeb48] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
  8. 0000002E98DFECA0 00007fff12dd2ca3 xxx.SqliteHelper.Insert[[System.__Canon, System.Private.CoreLib]](System.__Canon, System.String ByRef)
  9. ...
  10. 0000002E98DFF220 00007fff136902b6 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
  11. 0000002E98DFF2D0 00007fff12d1a12b System.Threading.ThreadPoolWorkQueue.Dispatch()
  12. 0000002E98DFF360 00007fff136de091 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
  13. 0000002E98DFF6B0 00007fff7115aed3 [DebuggerU2MCatchHandlerFrame: 0000002e98dff6b0]
复制代码
从卦中可以看到当前卡在 SqliteHelper.Insert 方法上,这到底是何方神圣?赶紧看一下代码。


用 Task.Run 去跑一个异步逻辑,是一个编程大坑,一旦这个 Task.Run 运行比较慢或者前端请求比较大,很容易造成线程饥饿,从这个程序中的 SetBlob 方法来看,就是将 byte[] 丢到 SqlLite 里,以是这个非托管内存走漏其实是 Sqlite 在非托管层持有的数据。
挖到了根子上的原因之后,解决办法就比较简朴了。

  • 只管的批量化Insert,不要用 foreach 一条一条的 Insert
  • 用单独线程队列化处置惩罚,不要用偷懒式 Task.Run
三:总结

这次分析之旅是典范的 在绝望中寻找希望,调试者必要具备冷静冷静的心态,坚持不放弃最终在 内存段 的 TEB 上找到了寻找原形的突破口。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大连密封材料

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表