一:背景

1. 讲故事

前几天有位敌人加wx说他的程序遭逢了内存暴涨,求助如何剖析?

和这位敌人聊下来,这个dump也是取自一个HIS零碎,如敌人所说我这真的是和医院杠上了,这样也好,给本人攒点资源,好了,不扯了,上windbg谈话。

二: windbg 剖析

1. 托管还是非托管?

既然是内存暴涨,那就看看以后过程的 commit 内存有多大?

0:000> !address -summary--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotalMEM_FREE                                174     7ffe`baac0000 ( 127.995 TB)          100.00%MEM_COMMIT                             1153        1`33bd3000 (   4.808 GB)  94.59%    0.00%MEM_RESERVE                             221        0`1195d000 ( 281.363 MB)   5.41%    0.00%

能够看出大略占了 4.8G,接下来再看看托管堆内存。

0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x00000207a4fc48c8generation 1 starts at 0x00000207a3dc3138generation 2 starts at 0x0000020697fc1000ephemeral segment allocation context: none------------------------------GC Heap Size:            Size: 0x1241b3858 (4900730968) bytes.

从最初一行能够看出托管堆占用 4900730968/1024/1024/1024=4.5G,两个指标一比对,原来是托管内存出问题了,这下好办了。。。

2. 查看托管堆

既然内存是被托管堆吃掉了,那就看看托管堆上到底都有些什么货色???

0:000> !dumpheap -statStatistics:              MT    Count    TotalSize Class Name...00007ffd00397b98  1065873    102323808 System.Data.DataRow00000206978b8250  1507805    223310768      Free00007ffd20d216b8  4668930    364025578 System.String00007ffd20d22aa8      797    403971664 System.String[]00007ffd20d193d0   406282   3399800382 System.Byte[]Total 9442152 objects

不看不晓得,一看吓一跳,System.Byte[] 差不多占用了 3.3 G 内存,也就是说 gc 堆差不多都被它吃掉了,依据教训必定是有个什么大对象,那接下来怎么剖析呢?除了用脚本对 byte[] 进行暴力分组统计之外,纯人肉还有其余的技巧吗? 当然有,能够用 !heapstat 察看下这些对象在托管堆上的代信息。

0:000> !heapstatHeap             Gen0         Gen1         Gen2          LOHHeap0         2252000     18880400   3968704192    910894376Free space:                                                 PercentageHeap0           43128       770160    185203264     39849984SOH:  4% LOH:  4%

从图中能够看出,以后的大头在 Gen2 上,接下来能够用 eeheap -gc 去找 Gen2 的段地址区间,从而最小化的显示heap上内容。

0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x00000207a4fc48c8generation 1 starts at 0x00000207a3dc3138generation 2 starts at 0x0000020697fc1000ephemeral segment allocation context: none         segment             begin         allocated              size0000020697fc0000  0000020697fc1000  00000206a7fbec48  0xfffdc48(268426312)00000206bbeb0000  00000206bbeb1000  00000206cbeaef50  0xfffdf50(268427088)00000206ccc40000  00000206ccc41000  00000206dcc3f668  0xfffe668(268428904)00000206dcc40000  00000206dcc41000  00000206ecc3f098  0xfffe098(268427416)0000020680000000  0000020680001000  000002068ffff8c0  0xfffe8c0(268429504)00000206ff4d0000  00000206ff4d1000  000002070f4cf588  0xfffe588(268428680)000002070f4d0000  000002070f4d1000  000002071f4cf9f0  0xfffe9f0(268429808)000002071f4d0000  000002071f4d1000  000002072f4cfef0  0xfffeef0(268431088)000002072f4d0000  000002072f4d1000  000002073f4cf748  0xfffe748(268429128)000002073f4d0000  000002073f4d1000  000002074f4ce900  0xfffd900(268425472)00000207574d0000  00000207574d1000  00000207674cfe70  0xfffee70(268430960)00000207674d0000  00000207674d1000  00000207774ceaf8  0xfffdaf8(268425976)00000207774d0000  00000207774d1000  00000207874cf270  0xfffe270(268427888)00000207874d0000  00000207874d1000  00000207974cf7a8  0xfffe7a8(268429224)00000207974d0000  00000207974d1000  00000207a51ea5a8  0xdd195a8(231839144)

一般来说,第一个 segment 是给 gen0 + gen1 的,后续的 segment 就是 gen2,接下来我就选 segment: 00000206dcc41000 - 00000206ecc3f098 ,而后应用 !dumpheap 导出该区间的所有对象。

0:000> !dumpheap -stat 00000206dcc41000 00000206ecc3f098Statistics:              MT    Count    TotalSize Class Name00007ffd00397b98   191803     18413088 System.Data.DataRow00007ffd20d216b8   662179     37834152 System.String00007ffd20d193d0    23115    187896401 System.Byte[]

从这个内存段上看,Byte[] 有 2.3w 个,还不算多,全副dump进去看看有什么特色。

0:000> !dumpheap -mt 00007ffd20d193d0 00000206dcc41000 00000206ecc3f098         Address               MT     Size00000206dcc410e8 00007ffd20d193d0     8232     00000206dcc43588 00007ffd20d193d0     8232     00000206dcc45a48 00007ffd20d193d0     8232     00000206dcc47d78 00007ffd20d193d0     8232     00000206dcc4a028 00007ffd20d193d0     8232     00000206dcc4c4b0 00007ffd20d193d0     8232     00000206dcc4eb08 00007ffd20d193d0     8232     00000206dcc50e88 00007ffd20d193d0     8232     00000206dcc535b0 00007ffd20d193d0     8232     00000206dcc575d8 00007ffd20d193d0     8232     00000206dcc5a5a8 00007ffd20d193d0     8232     00000206dcc5cbf8 00007ffd20d193d0     8232     00000206dcc5eef8 00007ffd20d193d0     8232     00000206dcc611f8 00007ffd20d193d0     8232     00000206dcc634e8 00007ffd20d193d0     8232     00000206dcc657f0 00007ffd20d193d0     8232     00000206dcc67af8 00007ffd20d193d0     8232     00000206dcc69e00 00007ffd20d193d0     8232   ...

我去,99% 都是 8232byte,原来都是些 8k 的byte数组,那到底谁在应用它,用 !gcroot 查一下援用根。

0:000> !gcroot 00000206dcc410e8Thread 8c1c:        rsi:             ->  00000206983d5730 System.ServiceProcess.ServiceBase[]                ...            ->  000002069dcb6d38 OracleInternal.ConnectionPool.OraclePool                ...            ->  000002069dc949c0 OracleInternal.TTC.OraBufReader            ->  000002069dc94a70 System.Collections.Generic.List`1[[OracleInternal.Network.OraBuf, Oracle.ManagedDataAccess]]            ->  00000206ab8c2200 OracleInternal.Network.OraBuf[]            ->  00000206dcc41018 OracleInternal.Network.OraBuf            ->  00000206dcc410e8 System.Byte[]

从援用链来看,貌似是被 OracleInternal.Network.OraBuf[] 持有着,这就很纳闷了,难道是 Oracle Sdk 出的bug把内存给搞崩了? 好奇心来了,看一下元素个数和size各是多少?

0:000> !do 00000206ab8c2200Name:        OracleInternal.Network.OraBuf[]MethodTable: 00007ffcc7833c68EEClass:     00007ffd20757728Size:        4194328(0x400018) bytesArray:       Rank 1, Number of elements 524288, Type CLASS (Print Array)Fields:None0:000> !objsize 00000206ab8c2200sizeof(00000206ab8c2200) = -1086824024 (0xbf3861a8) bytes (OracleInternal.Network.OraBuf[])

以后数组有 52w ,totalsize间接正数了。

3. 寻找问题代码

晓得景象之后,接下来用 ILSpy 把 Oracle SDK 反编译看看,最终一比对,如下图所示:

原来m_tempOBList是内存暴涨的罪魁祸首,这就很难堪了,它为什么会暴涨? 为什么不开释? 因为我对 Oracle 也不相熟,只能求助于神奇的 StackOverflow,我去,还真有咫尺沦落人,Huge managed memory allocation when reading (iterating) data with DbDataReader

大略是说这种景象是 Oracle SDK 在读取 Clob 类型的字段有一个bug,解决办法也很简略,用完后就开释,详情参见如下图:

4. 寻找假相

既然帖子上是说读取 Clob 类型出的问题,那就把所有线程栈都调进去,看看此时的线程栈中是否有 Clob 的踪影?

从线程栈上看,代码是通过 ToDataTable 办法将 IDataReader 转成 DataTable,在转换过程中读取了大字段,天然就有了 GetCompleteClobData,也就是说完满命中帖子所说,为了让论断更精确,我就去挖一下以后的 DataReader 曾经读了多少行了?

0:028> !clrstack -aOS Thread Id: 0xbab0 (28)000000e78ef7d520 00007ffd00724458 System.Data.DataTable.Load(System.Data.IDataReader, System.Data.LoadOption, System.Data.FillErrorEventHandler)    PARAMETERS:        this = <no data>        reader (<CLR reg>) = 0x00000206a530ac20        loadOption = <no data>        errorHandler = <no data>0:028> !do 0x00000206a530ac20Name:        Oracle.ManagedDataAccess.Client.OracleDataReaderMethodTable: 00007ffcc7933b10EEClass:     00007ffcc78efd30Size:        256(0x100) bytesFile:        D:\xxx.dllFields:00007ffd20d23e98  4000337       d0         System.Int32  1 instance          1061652 m_RowNumber

从 m_RowNumber 看,曾经读取了 106w 行,一次性读取100w+的记录不常见,如果还有大字段的话,那也是了。

三:总结

综合来看这次事变是因为一次性读取含有大字段的百万级数据到DataTable引发,解决方案很简略,本人通过 for 读取 DataReader,在解决完 OracleClob 类型之后马上开释,参考帖子代码:

var item = oracleDataReader.GetOracleValue(columnIndex);if (item is OracleClob clob){    if (clob != null)    {        // use clob.Value ...        clob.Close();    }}

更多高质量干货:参见我的 GitHub: dotnetfly