|
一:背景
1. 讲故事
前些天有位朋友找到我,说他的程序几天内存就要爆一次,不知道咋回事,找不出原因,让我帮忙看一下,这种问题分析dump是最简单粗暴了,拿到dump后接下来就是一顿分析。
二:WinDbg 分析
1. 程序为什么会暴
程序既然会爆,可能是虚拟地址受限,也可能是系统内存不足,可以用 !address -summary 观察下。- 0:037> !address -summary
- --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- <unknown> 866 53577000 ( 1.302 GB) 69.38% 65.11%
- Image 2244 16ee2000 ( 366.883 MB) 19.09% 17.91%
- Heap 222 8adc000 ( 138.859 MB) 7.23% 6.78%
- Free 460 7e14000 ( 126.078 MB) 6.16%
- Stack 255 5150000 ( 81.312 MB) 4.23% 3.97%
- TEB 85 db000 ( 876.000 kB) 0.04% 0.04%
- Other 20 79000 ( 484.000 kB) 0.02% 0.02%
- PEB 1 3000 ( 12.000 kB) 0.00% 0.00%
- ...
- --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- MEM_COMMIT 2900 64906000 ( 1.571 GB) 83.72% 78.57%
- MEM_RESERVE 793 138d6000 ( 312.836 MB) 16.28% 15.28%
- MEM_FREE 460 7e14000 ( 126.078 MB) 6.16%
- ...
复制代码 从卦中可以明显的看出,这又是一例经典的32bit程序受到了2G的内存限制,按往期经验来说解决办法比较简单,改成大地址或者x64即可。
哈哈,既然要分享这篇,自然就不是这么简单的事情,这需要我们排查这个溢出是不是程序的bug导致的,如果是那还得继续找原因。
2. 是程序bug导致的吗
要想搞清楚这个问题,需要去分析各处的内存占用,比如托管堆,可以用 !eeheap -gc 观察。- 0:037> !eeheap -gc
- Number of GC Heaps: 1
- generation 0 starts at 0x49fd10a8
- generation 1 starts at 0x49fd1000
- generation 2 starts at 0x03381000
- ephemeral segment allocation context: none
- segment begin allocated size
- 03380000 03381000 0437ff88 0xffef88(16773000)
- 23e60000 23e61000 24e5ff88 0xffef88(16773000)
- 0b510000 0b511000 0c50ff88 0xffef88(16773000)
- ...
- 7be20000 7be21000 7cbbdb60 0xd9cb60(14273376)
- 49fd0000 49fd1000 4afcfe08 0xffee08(16772616)
- Large object heap starts at 0x04381000
- segment begin allocated size
- 04380000 04381000 04a67b50 0x6e6b50(7236432)
- Total Size: Size: 0x39738ad4 (963873492) bytes.
- ------------------------------
- GC Heap Size: Size: 0x39738ad4 (963873492) bytes.
复制代码 从卦中可以看到,托管堆占用963M,并且产生了很多的16M的segment,这就表明当前的托管堆吃掉了内存,接下来的问题是为什么托管堆吃了那么多的内存呢?那就只能用 !dumpheap -stat 去观察下托管堆的对象布局咯。- 0:037> !dumpheap -stat
- Statistics:
- MT Count TotalSize Class Name
- ...
- 717c8b4c 264594 11642136 System.Threading.ExecutionContext
- 717cd044 265930 13034088 System.Collections.Hashtable+bucket[]
- 717ccff4 265854 13824408 System.Collections.Hashtable
- 71761c34 268005 17152320 System.Threading.OverlappedData
- 70d73c10 264469 26446900 System.Net.Sockets.OverlappedAsyncResult
- 717cdd04 280225 293649193 System.Byte[]
- 013a9f98 269886 540566904 Free
- Total 3880354 objects
复制代码 从卦中可以看到当前托管堆有 26.8w 的 OverlappedData 对象,这是一个非常明显的异常信号,熟悉这块的朋友应该知道,这个东西常常和异步打交道,也就表示当前程序可能有高达 26.8w 的异步请求可能没有得到响应,要想找到这个答案,就需要对 OverlappedData 进行穿刺。
3. OverlappedData 穿刺检查
对 OverlappedData 穿刺的目的就是要活检内部的 AsyncCallback 回调函数,看看到底是良性还是恶性的,相关命令如下:- 0:037> !dumpheap -stat
- ...
- 34f38ac4 71761c34 64
- 34f39088 71761c34 64
- ...
- 0:037> !mdt 34f39088
- 34f39088 (System.Threading.OverlappedData)
- m_asyncResult:33e8aafc (System.Net.Sockets.OverlappedAsyncResult)
- m_iocb:03c077a0 (System.Threading.IOCompletionCallback)
- ...
- m_nativeOverlapped:(System.Threading.NativeOverlapped) VALTYPE (MT=7176dfe0, ADDR=34f390b0)
- 0:037> !mdt 33e8aafc
- 33e8aafc (System.Net.Sockets.OverlappedAsyncResult)
- m_AsyncObject:03c71d44 (System.Net.Sockets.Socket)
- m_AsyncState:33e8aaec (xxx)
- m_AsyncCallback:03e8f214 (System.AsyncCallback)
- ...
- 0:037> !mdt 03e8f214
- 03e8f214 (System.AsyncCallback)
- _target:03c065a8 (xxx)
- _methodPtr:19432480 (System.IntPtr)
- 0:037> u 19432480
- 19432480 e933932102 jmp 1b64b7b8
- 19432485 5f pop edi
- ...
- 0:037> !ip2md 1b64b7b8
- MethodDesc: 131605ac
- Method Name: xxxDevices.ReceiveCallback(System.IAsyncResult)
复制代码 卦中的信息量还是蛮大的,可以看到这是一个和 Socket 相关的异步函数,并且也成功找到了 xxxDevices.ReceiveCallback 回调函数,接下来就是检查下这个方法附近的业务逻辑,由于代码会涉及到一些隐私,我就多模糊一点,请见谅,截图如下:
仔细阅读这段代码,他是想用异步的方式一次次的用byte[1024]去丈量一段可能的大数据,直到这个 Stream 不能再读了,所以用了 if (stream.CanRead) 判断。
对 Socket 编程比较熟悉的朋友相信很快就能发现问题,判断 Stream 中的数据是否读完应该用 DataAvailable 属性,而不是 CanRead,比如下面这段正确的代码:
最后再贴VS中对 CanRead 和 DataAvailable 属性的解释。- //
- // Summary:
- // Gets a value that indicates whether the System.Net.Sockets.NetworkStream supports
- // reading.
- //
- // Returns:
- // true if data can be read from the stream; otherwise, false. The default value
- // is true.
- public override bool CanRead { get; }
- //
- // Summary:
- // Gets a value that indicates whether data is available on the System.Net.Sockets.NetworkStream
- // to be read.
- //
- // Returns:
- // true if data is available on the stream to be read; otherwise, false.
- //
- public virtual bool DataAvailable { get; }
复制代码 三:总结
这个事故非常有意思,一个简简单单的 CanRead 误用就对程序造成了毁灭性的打击,这也警示大家在用某个属性某个方法前,一定要先搞清楚它到底是怎么玩的。
来源:https://www.cnblogs.com/huangxincheng/Undeclared/17972069
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|