一:背景
1.講故事
前段時間有位朋友微信找到我,說他的程序偶爾會出現內存溢出崩潰,讓我幫忙看下是怎么回事,咨詢了下程序是 x86 部署,聽到這個詞其實心里已經有了數,不管怎么樣還是用 windbg 分析一下。
二:WinDbg 分析
1. x86 程序意味著什么
x86
程序意味著程序默認只能吃到 2G 的內存,或者說只能用 2G 的虛擬地址,這種類型的程序很容易出現 虛擬地址緊張
造成崩潰,那怎么去驗證程序只能吃 2G 內存呢?通常有兩種做法:
1) 使用 !dh 查看 PE 頭
可以用 lm
找到 exe 模塊,然后使用 !dh Device_xxx
觀察 PE 頭,代碼如下:
0:000>?lm
start????end????????module?name
00360000?0099a000???xxxDevice?C?(service?symbols:?CLR?Symbols?without?PDB)????????
157f0000?15abf000???QQPinyin???(export?symbols)???????QQPinyin.ime
....0:000>?!dh?xxxDeviceFile?Type:?EXECUTABLE?IMAGE
FILE?HEADER?VALUES14C?machine?(i386)3?number?of?sections
6305F7E8?time?date?stamp?Wed?Aug?24?18:05:28?20220?file?pointer?to?symbol?table0?number?of?symbolsE0?size?of?optional?header102?characteristicsExecutable32?bit?word?machine
...
最后一行的 32 bit word machine
表示是純純 x86
,但在我的分析旅行中,這種也不是特別準,曾經遇到程序開啟了 大地址
,最后也只能吃 2G 內存,這就很奇葩了,所以更準的方式就是用 !address
看內存段。
使用
!address
查看內存段
這種做法萬無一失,輸出如下:
0:000>?!addressBaseAddr?EndAddr+1?RgnSize?????Type???????State?????????????????Protect?????????????Usage
-----------------------------------------------------------------------------------------------
+????????0???360000???360000?????????????MEM_FREE????PAGE_NOACCESS??????????????????????Free???
...
+?7ffe1000?7ffec000?????b000?????????????MEM_FREE????PAGE_NOACCESS??????????????????????Free???????
+?7ffec000?7ffed000?????1000?MEM_PRIVATE?MEM_COMMIT??PAGE_READONLY??????????????????????<unknown>??[HalT............]
+?7ffed000?7fff0000?????3000?????????????MEM_FREE????PAGE_NOACCESS??????????????????????Free???????0:000>???7fff0000?/0x100000
Evaluate?expression:?2047?=?000007ff
卦中最后一個內存段地址為 7fff0000
,也就是 2G 的意思,所以最好的辦法就是讓朋友開啟大地址解決,那大地址怎么開,用 anycpu 編譯即可,但有很多朋友反饋用 anycpu 的話,很多 C++ 的鏈接庫會報錯,所以更好的做法是參考這篇:https://www.cnblogs.com/huangxincheng/p/15671957.html
到這里,貌似就可以結案了。。。
2. 真的要讓 2G 地址背鍋嗎
開啟大地址可以讓程序吃到更多的內存,這個不假,但 放之四海而皆準
也不見得,言外之意還得分析下內存是怎么被吃掉的?如果是程序本身的問題,不斷的侵蝕內存,再多的內存也不夠用,對吧。
作為一個負責任的 調試博主,還是不要簡單忽悠過去,接下來用 !address -summary
觀察下內存布局。
0:000>?!address?-summary---?Usage?Summary?----------------?RgnCount?-----------?Total?Size?--------?%ofBusy?%ofTotal
<unknown>??????????????????????????????1221??????????551c4000?(???1.330?GB)??79.82%???66.49%
Free????????????????????????????????????258??????????155dd000?(?341.863?MB)???????????16.69%
Image???????????????????????????????????916???????????b1c6000?(?177.773?MB)??10.42%????8.68%
Stack???????????????????????????????????303???????????62c0000?(??98.750?MB)???5.79%????4.82%
Heap????????????????????????????????????129???????????426f000?(??66.434?MB)???3.89%????3.24%
TEB?????????????????????????????????????101?????????????f7000?(?988.000?kB)???0.06%????0.05%
Other????????????????????????????????????12?????????????60000?(?384.000?kB)???0.02%????0.02%
PEB???????????????????????????????????????1??????????????3000?(??12.000?kB)???0.00%????0.00%---?Type?Summary?(for?busy)?------?RgnCount?-----------?Total?Size?--------?%ofBusy?%ofTotal
MEM_PRIVATE????????????????????????????1437??????????561fd000?(???1.346?GB)??80.77%???67.29%
MEM_IMAGE??????????????????????????????1180???????????d23f000?(?210.246?MB)??12.32%???10.27%
MEM_MAPPED???????????????????????????????66???????????75d7000?(?117.840?MB)???6.91%????5.75%---?State?Summary?----------------?RgnCount?-----------?Total?Size?--------?%ofBusy?%ofTotal
MEM_COMMIT?????????????????????????????2113??????????5ce03000?(???1.451?GB)??87.10%???72.56%
MEM_FREE????????????????????????????????258??????????155dd000?(?341.863?MB)???????????16.69%
MEM_RESERVE?????????????????????????????570???????????dc10000?(?220.062?MB)??12.90%???10.75%...
從輸出看,當前提交內存為:MEM_COMMIT = 1.45G
,以我的經驗來說, 1.2G
是一個警戒線,一旦過了,程序崩潰的概率會幾何倍提升。
從 <unknown>=1.33G
來看,內存可能都被 GC堆 或者 VirtualAlloc
吃掉了,為了進一步驗證,需要看下托管堆。
0:000>?!eeheap?-gc
Number?of?GC?Heaps:?1
generation?0?starts?at?0x5ff45f5c
generation?1?starts?at?0x5fee1000
generation?2?starts?at?0x02d81000
ephemeral?segment?allocation?context:?nonesegment?????begin??allocated??????size
02d80000??02d81000??03d7fc50??0xffec50(16772176)
098c0000??098c1000??0a8bfc10??0xffec10(16772112)
0abf0000??0abf1000??0bbefc90??0xffec90(16772240)
0e0a0000??0e0a1000??0f09ff40??0xffef40(16772928)
13640000??13641000??1463fee8??0xffeee8(16772840)
17e40000??17e41000??18e3fff8??0xffeff8(16773112)
...
5fee0000??5fee1000??603b86e4??0x4d76e4(5076708)
Large?object?heap?starts?at?0x03d81000segment?????begin??allocated??????size
03d80000??03d81000??04cd0f70??0xf4ff70(16056176)
3e8f0000??3e8f1000??3f88bd80??0xf9ad80(16362880)
3f8f0000??3f8f1000??4075eeb0??0xe6deb0(15130288)
Total?Size:??????????????Size:?0x44c33258?(1153643096)?bytes.
------------------------------
GC?Heap?Size:????Size:?0x44c33258?(1153643096)?bytes.
從卦中的 GC Heap Size= 1.15G
來看,原來都是被 GCHeap 給弄沒了,它吃掉了這么多內存是正常還是異常現象呢?這個還是取決于程序的業務邏輯,比如人家有一個小緩存什么的。
3. 真的要讓程序背鍋嗎
既然分析到這里,含著淚也得分析下去,可以使用 !dumpheap -stat
看下托管堆使用。
0:000>?!dumpheap?-stat
Statistics:MT????Count????TotalSize?Class?Name
71430958??1420366?????66249092?System.Int32[]
...
174f4300??6852848????109645568?xxx.Mes.xxxPlatInfo
174f4194??6852848????164468352?xxx.Mes.Platxxx
7142eb40??6923571????210298518?System.String
00dbb9a0??1788917????434963034??????Free
如果你有足夠的分析經驗,一看就能看出問題,比如 xxx.Mes.xxxPlatInfo
和 xxx.Mes.Platxxx
對象高達 685w
,對象之間的排列布局很容易造成大量 Free 塊,也叫做堆碎片化,真的很難看,類似下面這樣。
0:000>?!dumpheap?5cee1000??5dede324Address???????MT?????Size...
5cee17c8?00dbb9a0???????38?Free
5cee17f0?174f4194???????24?????
5cee1808?7142eb40???????30?????
5cee1828?174f4300???????16?????
5cee1838?00dbb9a0???????10?Free
5cee1844?174f4194???????24?????
5cee185c?7142eb40???????30?????
5cee187c?174f4300???????16?????
5cee188c?00dbb9a0???????46?Free
5cee18bc?174f4194???????24?????
5cee18d4?7142eb40???????30?????
5cee18f4?174f4300???????16?????
5cee1904?00dbb9a0???????10?Free
5cee1910?174f4194???????24?????
5cee1928?7142eb40???????30?????
5cee1948?174f4300???????16?????
5cee1958?00dbb9a0???????50?Free
5cee198c?174f4194???????24?????
5cee19a4?7142eb40???????30?????
5cee19c4?174f4300???????16?????
5cee19d4?00dbb9a0??????130?Free
...
接下來在 xxx.Mes.xxxPlatInfo
中抽一個對象觀察它的引用根,為什么沒有被 GC 回收。

從圖中可以看到,它被 xxxPlatInfo
類下的 ConcurrentDictionary 中的 List 持有,我翻看了幾個,Size 都比較大,比如下面輸出:
0:000>?!do?0ea19634
Name:????????System.Collections.Generic.List`1[[xxx]]
MethodTable:?174f4350
EEClass:?????71006b4c
Size:????????24(0x18)?bytes
Fields:MT????Field???Offset?????????????????Type?VT?????Attr????Value?Name
7143e0fc??400188f????????4?????System.__Canon[]??0?instance?04bd0f60?_items
71430994??4001890????????c?????????System.Int32??1?instance???200367?_size
71430994??4001891???????10?????????System.Int32??1?instance???200367?_version
7142eee0??4001892????????8????????System.Object??0?instance?00000000?_syncRoot
7143e0fc??4001893????????4?????System.__Canon[]??0???static??<no?information>0:000>?!DumpObj?/d?0e6b9888
Name:???????System.Collections.Generic.List`1[[xxx]]
MethodTable:?174f4350
EEClass:?????71006b4c
Size:????????24(0x18)?bytes
Fields:MT????Field???Offset?????????????????Type?VT?????Attr????Value?Name
7143e0fc??400188f????????4?????System.__Canon[]??0?instance?3f78bd70?_items
71430994??4001890????????c?????????System.Int32??1?instance???171806?_size
71430994??4001891???????10?????????System.Int32??1?instance???171806?_version
7142eee0??4001892????????8????????System.Object??0?instance?00000000?_syncRoot
7143e0fc??4001893????????4?????System.__Canon[]??0???static??<no?information>
將這些信息反饋給朋友后,朋友說 List 這么多是有問題的,排查之后是 List 在多線程情況下有問題,修正之后問題得到解決。
三:總結
這次事故主要是由于朋友在處理線程安全集合 ConcurrentDictionary<xxx, List<xxx>>
的過程中,對其中的 List<xxx>
沒有合理的線程安全處理,導致數據的異常暴增,最終把緊張的 2G 虛擬地址用盡。
教訓就是:key 線程安全了, value 也要記的安全哦!