一:背景
1. 講故事
前段時間有位朋友找到我,說他的程序內存占用比較大,尋求如何解決,截圖就不發了,分析下來我感覺除了程序本身的問題之外,.NET5
在內存管理方面做的也不夠好,所以有必要給大家分享一下。
二:WinDbg 分析
1. 托管還是非托管泄漏
這個還是老規矩 !address -summary
和 !eeheap -gc
組合命令排查一下。
0:000>?!address?-summaryMapping?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...
---?State?Summary?----------------?RgnCount?-----------?Total?Size?--------?%ofBusy?%ofTotal
MEM_FREE????????????????????????????????426?????7df8`af1ce000?(?125.971?TB)???????????98.42%
MEM_RESERVE?????????????????????????????619??????206`01b9c000?(???2.023?TB)??99.75%????1.58%
MEM_COMMIT?????????????????????????????3096????????1`4f286000?(???5.237?GB)???0.25%????0.00%0:000>?!eeheap?-gc
Number?of?GC?Heaps:?16
------------------------------
...
Heap?15?(0000024AF6BAA2E0)
generation?0?starts?at?0x000002509729B538
generation?1?starts?at?0x000002509720B638
generation?2?starts?at?0x0000025096F91000
ephemeral?segment?allocation?context:?nonesegment?????????????begin?????????allocated?????????committed????allocated?size????committed?size
0000025096F90000??0000025096F91000??000002509B5AFB40??000002509DFE9000??0x461eb40(73526080)??0x7058000(117800960)
Large?object?heap?starts?at?0x00000250D6F91000segment?????????????begin?????????allocated?????????committed????allocated?size????committed?size
00000250D6F90000??00000250D6F91000??00000250DEB6AC60??00000250DEB6B000??0x7bd9c60(129866848)??0x7bda000(129867776)
Pinned?object?heap?starts?at?0x00000250E6F91000
00000250E6F90000??00000250E6F91000??00000250E75D94E0??00000250E75DA000??0x6484e0(6587616)??0x649000(6590464)
Allocated?Heap?Size:???????Size:?0xc840c80?(209980544)?bytes.
Committed?Heap?Size:???????Size:?0xec32000?(247668736)?bytes.
------------------------------
GC?Allocated?Heap?Size:????Size:?0xd6904dd8?(3599781336)?bytes.
GC?Committed?Heap?Size:????Size:?0x11884b000?(4706316288)?bytes.
從卦中指標看:5.2G
和 4.7G
,很明顯問題出在了托管層,但如果你細心的話,你會發現這 4.7G
是 commit 內存,其實真正占用的只有 3.5G
,言外之意有 1.2G
的空間其實屬于 Commit 區,也就是為了少向 OS 申請內存而虛占的一部分空間,畫個簡圖就像下面這樣:

這也是我第一次看到 Alloc
和 Commit
差距有這么大。
2. 探究托管內存占用
首先看下 3.5G
內存這塊,這個分析比較簡單,直接看托管堆就好了。
0:000>?!dumpheap?-stat
Statistics:MT????Count????TotalSize?Class?Name
...
00007ffa19e64808????25804?????36125600?xxxx.MongoDB.Entity.GeneratorMongodb
0000024af68aa2c0????20517????630474976??????Free
00007ffa1947bf30????52477????654558722?System.Byte[]
00007ffa194847f0?????1921???1044818774?System.Char[]
00007ffa19437a90???673850???1116597742?System.String
從輸出信息看,主要還是被 String,Char[],Byte[]
占用了,根據經驗,這三個組合在一塊,大多是存了什么字節流在內存中,比如 Pdf,Image ,然后在內存中倒來倒去就成這個樣子了。
接下來在 char[]
中抽一些 obj 看一下,果然大多是 jpg
。
0:000>?!DumpObj?/d?00000250da9d3618
Name:????????System.Char[]
MethodTable:?00007ffa194847f0
EEClass:?????00007ffa19484770
Size:????????11990052(0xb6f424)?bytes
Array:???????Rank?1,?Number?of?elements?5995014,?Type?Char?(Print?Array)
Content:?????data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAA4QAAASwCAYAAACjAoQOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DA
Fields:
None
0:000>?!DumpObj?/d?00000250db542a60
Name:????????System.Char[]
MethodTable:?00007ffa194847f0
EEClass:?????00007ffa19484770
Size:????????15667860(0xef1294)?bytes
Array:???????Rank?1,?Number?of?elements?7833918,?Type?Char?(Print?Array)
Content:?????data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAA4QAAASwCAYAAACjAoQOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DA
Fields:
None
可以看到,3.2G 的內存大多是被 圖片
所占用,朋友反饋是把 圖片 存到數據庫所致,好了,這一塊就分析到這里,分析思路也很明顯,接下來探究下 alloc 和 commit 的問題。
3. 為什么 alloc 和 commit 差距這么大
一般而言,差距大有以下幾點誘因所致。
segment 越大,commit 預設的區域就越大
根據官方文檔的定義,segment 的大小取決于 cpu核數 和 程序的位數,截圖如下:

有了這個指標,怎么到 dump去找各自數據呢,用 !eeversion
看下 heap 的個數以及觀察下內存地址的長度就好啦。
0:000>?!eeversion
5.0.621.22011?free
5,0,621,22011?@Commit:?478b2f8c0e480665f6c52c95cd57830784dc9560
Server?mode?with?16?gc?heaps
SOS?Version:?6.0.5.7301?retail?build
可以看到,這個程序是用 64bit 跑在 16 核機器上,segment 上限為 1G 。
segment 越多,alloc 和 commit 累計差距就會越大
每個 segment 都差一點,那多個 segment 自然就累計出來了,接下來就找一下那些差距比較大的 segment。
Heap?0?(0000024AF685A500)segment?????????????begin?????????allocated?????????committed????allocated?size????committed?size
0000024AF6F90000??0000024AF6F91000??0000024AF83B6D28??0000024AFEB42000??0x1425d28(21126440)??0x7bb1000(129699840)
------------------------------
Heap?1?(0000024AF68819A0)segment?????????????begin?????????allocated?????????committed????allocated?size????committed?size
0000024B56F90000??0000024B56F91000??0000024B58507410??0000024B5D2E5000??0x1576410(22504464)??0x6354000(104153088)
------------------------------
Heap?4?(0000024AF688F770)segment?????????????begin?????????allocated?????????committed????allocated?size????committed?size
0000024C76F90000??0000024C76F91000??0000024C783BDBE8??0000024C7ECF7000??0x142cbe8(21154792)??0x7d66000(131489792)
------------------------------
Heap?6?(0000024AF68980A0)segment?????????????begin?????????allocated?????????committed????allocated?size????committed?size
0000024D36F90000??0000024D36F91000??0000024D38B87E78??0000024D3F881000??0x1bf6e78(29322872)??0x88f0000(143589376)
...
從輸出信息看,差距最大的是 Heap6
,高達 110M,那這 110M
差距是否合理呢?其實仔細想想也不太離譜,畢竟命中了上面提到的兩點,但我覺得這里的空間是不是還可以再智能的優化一下,再縮小一點?
4. Commit區能不能再小點?
能不能縮的再小一點,其實這是一種 CLR 智能算法的抉擇,Commit 區越大,申請對象的速度就越快,向 OS 申請內存的頻率就越低,反之 Commit 區越小,向 OS 再次申請內存的概率就越大,段的模型圖大概是這個樣子:

后來仔細想了下,既然 Commit 區多保留了 110M,那曾經肯定是某一個時刻突破過,后來因為成了垃圾對象,被 GC 回收了,但內存區域被GC私藏下來,所以程序肯定出現過 快出快進 的現象,接下來的想法就是用 writemem 把 alloc ~ commit
的內存區間給導出來看下,是不是有什么新發現。
0:000>?.writemem?D:\dumps\dump1\1.txt?0000024AF83B6D28?L?0x0678b2d8?
Writing?678b2d8?bytes.............

發現了很多類似這樣的信息,把這個信息提供給朋友后,朋友說他找到這塊問題了,是網站上用 NPOI ?數據導出
功能所致。
三:總結
其實這個 dump 給了我們兩方面的教訓。
不要將 image 放到 sqlserver 里,不僅占用sql的資源,讓程序也不堪重負,畢竟讀出去都是 byte[] ...
coreclr 雖然有自己的抉擇算法,如果再智能一點就好了,讓
commit ~ alloc
之間的差距更合理一點。