記一次 .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 objects
Handles:
Strong Handles: 174
Pinned Handles: 15
Async Pinned Handles: 25369
Ref Count Handles: 1
Weak Long Handles: 10681
Weak 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 0000000001000000
Address 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 該實現的效果。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3137/viewspace-2797538/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 記一次 .NET 某電廠Web系統 記憶體洩漏分析Web記憶體
- 記一次堆外記憶體洩漏分析記憶體
- 記一次 .NET 某外貿ERP 記憶體暴漲分析記憶體
- 記一次 .NET 某風控管理系統 記憶體洩漏分析記憶體
- 記一次 WinDbg 分析 .NET 某工廠MES系統 記憶體洩漏分析記憶體
- 記一次 .NET 某HIS系統後端服務 記憶體洩漏分析後端記憶體
- 記一次 .NET 某桌面奇俠遊戲 非託管記憶體洩漏分析遊戲記憶體
- 記一次 .NET 某智慧水廠API 非託管記憶體洩漏分析API記憶體
- 記一次 .NET 某智慧服裝智造系統 記憶體洩漏分析記憶體
- 記一次 .NET 某工控軟體 記憶體洩露分析記憶體洩露
- 記一次 .NET 某消防物聯網 後臺服務 記憶體洩漏分析記憶體
- 分析記憶體洩漏和goroutine洩漏記憶體Go
- 記憶體分析與記憶體洩漏定位記憶體
- valgrind 記憶體洩漏分析記憶體
- ANTS Memory Profiler - .NET記憶體洩漏分析工具記憶體
- .Net程式記憶體洩漏解析記憶體
- Android 記憶體洩漏分析Android記憶體
- PHP 記憶體洩漏分析定位PHP記憶體
- 記憶體洩漏記憶體
- .NET 記憶體洩漏的爭議記憶體
- 記一次使用 laravel-s 記憶體洩漏Laravel記憶體
- js記憶體洩漏JS記憶體
- Java記憶體洩漏Java記憶體
- webView 記憶體洩漏WebView記憶體
- Javascript記憶體洩漏JavaScript記憶體
- 一次 Java 記憶體洩漏的排查Java記憶體
- 記憶體洩漏和記憶體溢位記憶體溢位
- 記一次 .NET 某流媒體獨角獸 API 控制程式碼洩漏分析API
- linux程式之記憶體洩漏分析Linux記憶體
- Handler記憶體洩漏分析及解決記憶體
- Android 5.1 WebView記憶體洩漏分析AndroidWebView記憶體
- Node.js 中記憶體洩漏分析Node.js記憶體
- 記一次 .NET 某電子廠OA系統 非託管記憶體洩露分析記憶體洩露
- 記一次 .NET 某手術室行為資訊系統 記憶體洩露分析記憶體洩露
- 記一次 .NET 某電力系統 記憶體暴漲分析記憶體
- 【記憶體洩漏和記憶體溢位】JavaScript之深入淺出理解記憶體洩漏和記憶體溢位記憶體溢位JavaScript
- Android 記憶體洩漏Android記憶體
- Android記憶體洩漏Android記憶體