一:背景
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 回收。
![a25792e38a044b31a0e1ece6acdb4ac5.png](https://img-blog.csdnimg.cn/img_convert/a25792e38a044b31a0e1ece6acdb4ac5.png)
从图中可以看到,它被 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 也要记的安全哦!