一:背景
1. 讲故事
前段时间微信上有一位老朋友找到我,说他的程序跑着跑着内存会突然爆高,有时候会下去,有什么会下不去,怀疑是不是某些情况下存在内存泄露,让我帮忙分析一下,其实内存泄露方面的问题还是比较好解决的,看过这个dump之后觉得还是有一定的分享价值,拿出来和大家分享一下吧。
二:WinDbg 分析
1. 托管还是非托管泄露
这个首先是一定要确定的,否则就是南辕北辙,强调再多也不为过,可以用 !address -summary 观察一下。- 0:000> !address -summary
- --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- Free 208 7ffb`691cd000 ( 127.982 TB) 99.99%
- <unknown> 384 4`898d7000 ( 18.149 GB) 98.87% 0.01%
- Image 1015 0`0b053000 ( 176.324 MB) 0.94% 0.00%
- Stack 90 0`01700000 ( 23.000 MB) 0.12% 0.00%
- Heap 32 0`00bea000 ( 11.914 MB) 0.06% 0.00%
- Other 14 0`001e0000 ( 1.875 MB) 0.01% 0.00%
- TEB 23 0`0002e000 ( 184.000 kB) 0.00% 0.00%
- PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00%
- --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- MEM_PRIVATE 336 4`8ab0b000 ( 18.167 GB) 98.96% 0.01%
- MEM_IMAGE 1177 0`0b70f000 ( 183.059 MB) 0.97% 0.00%
- MEM_MAPPED 46 0`00c09000 ( 12.035 MB) 0.06% 0.00%
- --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- MEM_FREE 208 7ffb`691cd000 ( 127.982 TB) 99.99%
- MEM_COMMIT 1256 3`1f910000 ( 12.493 GB) 68.05% 0.01%
- MEM_RESERVE 303 1`77513000 ( 5.864 GB) 31.95% 0.00%
复制代码 从卦中看,当前程序的提交内存占用了 12G,NTHEAP 占用了 11M,基本上就可以断定这是一个托管内存的问题,看到这里还是非常开心的,由于托管层都带有类型数据,分析起来非常简单,接下来使用 !eeheap -gc 观察下托管堆。- 0:000> !eeheap -gc
- Number of GC Heaps: 1
- generation 0 starts at 0x00000000e42f26c8
- generation 1 starts at 0x00000000e42e85a0
- generation 2 starts at 0x0000000001d71000
- ephemeral segment allocation context: none
- segment begin allocated size
- 0000000001d70000 0000000001d71000 0000000011961b98 0x000000000fbf0b98(264178584)
- 000000005e540000 000000005e541000 000000006c2b6408 0x000000000dd75408(232215560)
- 0000000247ff0000 0000000247ff1000 0000000252d751f8 0x000000000ad841f8(181944824)
- 00000000e3ff0000 00000000e3ff1000 00000000f1e44850 0x000000000de53850(233125968)
- Large object heap starts at 0x0000000011d71000
- segment begin allocated size
- 0000000011d70000 0000000011d71000 0000000019d65098 0x0000000007ff4098(134168728)
- 000000001dad0000 000000001dad1000 0000000025ac6fd0 0x0000000007ff5fd0(134176720)
- 0000000028c50000 0000000028c51000 0000000030c318e8 0x0000000007fe08e8(134088936)
- 0000000030c50000 0000000030c51000 0000000038c35518 0x0000000007fe4518(134104344)
- 000000003e540000 000000003e541000 0000000046507ff0 0x0000000007fc6ff0(133984240)
- 0000000046540000 0000000046541000 000000004e5296d0 0x0000000007fe86d0(134121168)
- 000000004e540000 000000004e541000 000000005652af00 0x0000000007fe9f00(134127360)
- 0000000056540000 0000000056541000 000000005e50b050 0x0000000007fca050(133996624)
- ...
- 000000058fff0000 000000058fff1000 00000005d76c2910 0x00000000476d1910(1198332176)
- 000000064e820000 000000064e821000 0000000695cbe808 0x000000004749d808(1196021768)
- 000000013bff0000 000000013bff1000 00000001834c96d8 0x00000000474d86d8(1196263128)
- 0000000257ff0000 0000000257ff1000 000000029f44e7e0 0x000000004745d7e0(1195759584)
- Total Size 0x2dbe1b140(12278935872)
- ------------------------------
- GC Heap Size 0x2dbe1b140(12278935872)
复制代码 从卦中看,托管堆占用了 12G 内存,而且都是被 LOH 给吃掉了,离真相越来越近了,接下来就是用 !dumpheap -stat 观察下托管堆,看下到底是什么类型占的这么大?- 0:000> !dumpheap -stat
- total 477283 objects
- Statistics:
- MT Count TotalSize Class Name
- 0007ffeb28280b0 3516 365664 System.Reflection.RuntimeMethodInfo
- 00007ffea909df50 62 728528 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
- 00007ffeb285a610 14 804680 System.DateTime[]
- 00007ffea909c5e8 17061 1637856 System.Data.DataRow
- 00007ffeb2831180 1555 1978136 System.Int32[]
- 00007ffeb282d430 1108 6648296 System.Int64[]
- 00007ffeb2fce9d8 71 12821784 System.Decimal[]
- 00007ffeb282a060 366739 15054680 System.String
- 00007ffeb2817e50 7785 23534144 System.Object[]
- 00000000009a2e60 4084 4772737632 Free
- 00007ffeb28320a0 17400 7438877632 System.Byte[]
- ...
复制代码 从卦中看, System.Byte[] 数组只有 1.7W 个,居然占用了 7.4G 的内存,这说明有的 Byte[] 可能会非常大, 接下来就可以检索 >10M 的数组排列情况,输出如下:- 0:000> !dumpheap -mt 00007ffeb28320a0 -min 0n10485760
- Address MT Size
- 0000000029051018 00007ffeb28320a0 33554456
- 000000002d051078 00007ffeb28320a0 33554456
- 00000003e6a43a10 00007ffeb28320a0 16777240
- 0000000547ff1000 00007ffeb28320a0 1195710200
- 000000058fff1000 00007ffeb28320a0 1195710200
- 000000064e821000 00007ffeb28320a0 1195759552
- 000000013bff1000 00007ffeb28320a0 1195759552
- 0000000257ff1000 00007ffeb28320a0 1195759560
- total 8 objects
- Statistics:
- MT Count TotalSize Class Name
- 00007ffeb28320a0 8 6062585216 System.Byte[]
- Total 8 objects
复制代码 从卦中看,居然有 5 个将近 1.2 G 的 Byte[] 数组,有点吓人,抽几个看看吧。- 0:000> !gcroot 000000058fff1000
- Note: Roots found on stacks may be false positives. Run "!help gcroot" for
- more info.
- Scan Thread 0 OSTHread 39f4
- 0:000> !gcroot 0000000547ff1000
- Note: Roots found on stacks may be false positives. Run "!help gcroot" for
- more info.
- Scan Thread 0 OSTHread 39f4
- Scan Thread 2 OSTHread 2f6c
- Scan Thread 3 OSTHread 3354
- Scan Thread 4 OSTHread 3924
复制代码 从卦中看,他们都没有引用根,也就说明是等待 GC 回收的临时对象,有朋友就要问了,GC 为什么不回收呢?
2. GC 为什么不回收
了解 GC 的朋友应该知道,LOH 默认不启用压缩回收,只会做简单的标记清除,清除之后就会存在很多的空洞,只有相邻的空洞才会合并成一个更大的空洞,一旦有分配的对象超过所有的空洞大小,GC 也只能被迫commit更多的新空间给它存放,虽然此时存放不间隔的空闲内存已经足够。
如果大家有点懵,画个图大概就像下面这样。

从图中看,程序要分配 1.3G 的对象, 虽然 LOH 上有 2G 的空闲空间但就是放不下,无奈只能 commit 更多的内存来存放这个对象。
这就是为什么朋友看到的,有时候内存会暴涨,有时候会降下去的底层原理。
3. byte[] 到底是什么
要想查看 byte[] 的内容,可以用 db, dW 观察内存地址,参考如下:- 0:000> dW 0000000547ff1000 L100
- 00000005`47ff1000 20a0 b283 7ffe 0000 16da 4745 0000 0000 . ........EG....
- 00000005`47ff1010 0100 0000 ff00 ffff 01ff 0000 0000 0000 ................
- 00000005`47ff1020 0c00 0002 0000 534e 7379 6574 2e6d 6144 ......NSystem.Da
- 00000005`47ff1030 6174 202c 6556 7372 6f69 3d6e 2e32 2e30 ta, Version=2.0.
- 00000005`47ff1040 2e30 2c30 4320 6c75 7574 6572 6e3d 7565 0.0, Culture=neu
- 00000005`47ff1050 7274 6c61 202c 7550 6c62 6369 654b 5479 tral, PublicKeyT
- 00000005`47ff1060 6b6f 6e65 623d 3737 3561 3563 3136 3339 oken=b77a5c56193
- 00000005`47ff1070 6534 3830 0539 0001 0000 5315 7379 6574 4e089......Syste
- 00000005`47ff1080 2e6d 6144 6174 442e 7461 5461 6261 656c m.Data.DataTable
- 00000005`47ff1090 04d6 0000 4419 7461 5461 6261 656c 522e .....DataTable.R
- 00000005`47ff10a0 6d65 746f 6e69 5667 7265 6973 6e6f 4418 emotingVersion.D
- 00000005`47ff10b0 7461 5461 6261 656c 522e 6d65 746f 6e69 ataTable.Remotin
- 00000005`47ff10c0 4667 726f 616d 1374 6144 6174 6154 6c62 gFormat.DataTabl
- 00000005`47ff10d0 2e65 6154 6c62 4e65 6d61 1365 6144 6174 e.TableName.Data
- 00000005`47ff10e0 6154 6c62 2e65 614e 656d 7073 6361 1065 Table.Namespace.
- 00000005`47ff10f0 6144 6174 6154 6c62 2e65 7250 6665 7869 DataTable.Prefix
- 00000005`47ff1100 4417 7461 5461 6261 656c 432e 7361 5365 .DataTable.CaseS
- 00000005`47ff1110 6e65 6973 6974 6576 441e 7461 5461 6261 ensitive.DataTab
- 00000005`47ff1120 656c 632e 7361 5365 6e65 6973 6974 6576 le.caseSensitive
- 00000005`47ff1130 6d41 6962 6e65 1474 6144 6174 6154 6c62 Ambient.DataTabl
- 00000005`47ff1140 2e65 6f4c 6163 656c 434c 4449 4419 7461 e.LocaleLCID.Dat
- 00000005`47ff1150 5461 6261 656c 4d2e 6e69 6d69 6d75 6143 aTable.MinimumCa
- 00000005`47ff1160 6170 6963 7974 4419 7461 5461 6261 656c pacity.DataTable
- 00000005`47ff1170 4e2e 7365 6574 4964 446e 7461 5361 7465 .NestedInDataSet
- 00000005`47ff1180 4412 7461 5461 6261 656c 542e 7079 4e65 .DataTable.TypeN
- 00000005`47ff1190 6d61 1b65 6144 6174 6154 6c62 2e65 6552 ame.DataTable.Re
- 00000005`47ff11a0 6570 7461 6261 656c 6c45 6d65 6e65 1c74 peatableElement.
- 00000005`47ff11b0 6144 6174 6154 6c62 2e65 7845 6574 646e DataTable.Extend
- 00000005`47ff11c0 6465 7250 706f 7265 6974 7365 4417 7461 edProperties.Dat
- 00000005`47ff11d0 5461 6261 656c 432e 6c6f 6d75 736e 432e aTable.Columns.C
- 00000005`47ff11e0 756f 746e 4421 7461 5461 6261 656c 442e ount!DataTable.D
- 00000005`47ff11f0 7461 4361 6c6f 6d75 5f6e 2e30 6f43 756c ataColumn_0.Colu
- ....
复制代码 从 unicode 码来看,貌似是一个 DataTable 的序列化,看样子是序列化一个巨大的 DataTable 导致的内存暴涨,接下来我们到线程栈上找到有么有类似的操作,算是碰碰运气吧。

哈哈,从卦中看还真的有类似操作,用了 BinaryFormatter 做序列化。。。接下来观察下这个 DataTable 的 RowCount。- 0:013> !do 0000000002582cb8
- Name: System.Data.DataRowCollection+DataRowTree
- MethodTable: 00007ffea909d3f0
- EEClass: 00007ffea8f1ef68
- Size: 64(0x40) bytes
- (C:\Windows\assembly\GAC_64\System.Data\2.0.0.0__b77a5c561934e089\System.Data.dll)
- Fields:
- MT Field Offset Type VT Attr Value Name
- 0000000000000000 4000765 8 SZARRAY 0 instance 000000000bc79d98 _pageTable
- 00007ffeb2831180 4000766 10 System.Int32[] 0 instance 000000000bc79fb8 _pageTableMap
- 00007ffeb28312d0 4000767 18 System.Int32 1 instance 42 _inUsePageCount
- 00007ffeb28312d0 4000768 1c System.Int32 1 instance 1 nextFreePageLine
- 00007ffeb28312d0 4000769 20 System.Int32 1 instance 1245312 root
- 00007ffeb28312d0 400076a 24 System.Int32 1 instance 17054 _version
- 00007ffeb28312d0 400076b 28 System.Int32 1 instance 17055 _inUseNodeCount
- 00007ffeb28312d0 400076c 2c System.Int32 1 instance 0 _inUseSatelliteTreeCount
- 00007ffea957b578 400076d 30 System.Int32 1 instance 2 _accessMethod
复制代码 从卦中看当前的 _inUseNodeCount=1.7W ,这只是捕获到的,相信还有更大的,也算是找到问题原因了。
三:总结
在和朋友沟通之后,朋友也确认了确实有大 DataTable 的情况,目前业务需求没法绕过,不过在这种情况下也还是有办法的,大致如下:
1. 强制 LOH 压缩
虽然 LOH 上的对象移动会产生很大的内存流量,但适当的用一用压缩也不失为一个简单粗暴的办法。- GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
- GC.Collect();
复制代码 2. 分批次获取
一次性获取 1.7W 条,可以拆成诸如 2000 条一次,好处很明显,可以充分利用 LOH 上留下来的空洞区。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
|