乐趣区

关于.net:记一次-NET-某外贸Web站-内存泄漏分析

一:背景

1. 讲故事

上周四有位敌人加 wx 征询他的程序内存存在肯定水平的透露,并且无奈被 GC 回收,最终机器内存耗尽,很难堪。

沟通下来,这位敌人能力还是很不错的,也曾经做了初步的 dump 剖析,发现了托管堆上有 10w+ 的 byte[] 数组,并占用了大略 1.1G 的内存,在抽取几个 byte[] 的 gcroot 后发现没有援用,接下来就排查不上来了,尽管晓得问题可能在 byte[],但苦于找不到证据。😪😪😪

那既然这么信赖的找到我,我得要做一个绝对全面的输入报告,不能辜负大家的信赖哈,还是老规矩,上 windbg 谈话。

二:windbg 剖析

1. 排查透露源

看过我文章的老读者应该晓得,排查这种内存泄露的问题,首先要二分法找出到底是托管还是非托管出的问题,不便后续采取相应的应答措施。

接下来应用 !address -summary 看一下过程的提交内存。


||2:2:080> !address -summary

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                             573        1`5c191000 (5.439 GB)  95.19%    0.00%
MEM_IMAGE                              1115        0`0becf000 (190.809 MB)   3.26%    0.00%
MEM_MAPPED                               44        0`05a62000 (90.383 MB)   1.54%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                201     7ffe`9252e000 (127.994 TB)          100.00%
MEM_COMMIT                             1477        0`d439f000 (3.316 GB)  58.04%    0.00%
MEM_RESERVE                             255        0`99723000 (2.398 GB)  41.96%    0.00%

从卦象的 MEM_COMMIT 指标看:以后只有 3.3G 的内存占用,说实话,我个别都倡议 5G+ 是做内存透露剖析的最低门槛,毕竟内存越大,越容易剖析,接下来看一下 托管堆 的内存占用。


||2:2:080> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000002b37c0c48
generation 1 starts at 0x00000002b3781000
generation 2 starts at 0x0000000000cc1000

------------------------------
GC Heap Size:            Size: 0xbd322bb0 (3174181808) bytes.

能够看到,以后托管堆占用 3174181808/1024/1024/1024= 2.95G,哈哈,看到这个数,心里一阵狂喜,托管堆上的问题,对我来说差不多就探囊取物了。。。毕竟还没有失手过,接下来连忙排查一下托管堆,看下是哪里出的问题。

2. 查看托管堆

要想查看托管堆,能够应用 !dumpheap -stat 命令,上面我把 Top10 Size 给显示进去。


||2:2:080> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffd7e130ab8   116201     13014512 Newtonsoft.Json.Linq.JProperty
00007ffdd775e560    66176     16411648 System.Data.SqlClient._SqlMetaData
00007ffddbcc9da8    68808     17814644 System.Int32[]
00007ffddbcaf788    14140     21568488 System.String[]
00007ffddac72958    50256     22916736 System.Net.Sockets.SocketAsyncEventArgs
00007ffd7deb64b0      369     62115984 System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.ICustomAttributeProvider, mscorlib],[System.Type, mscorlib]][]
00007ffddbcc8610     8348    298313756 System.Char[]
00007ffddbcc74c0  1799807    489361500 System.String
000000000022e250   312151    855949918      Free
00007ffddbccc768   109156   1135674368 System.Byte[]

从下面的输入中能够看到,以后状元是 Byte[],榜眼是 Free,探花是 String,这里还是有一些经验之谈的,深究 Byte[]String 这种根底类型,投入产出比是不高的,毕竟大量的简单类型,它的内部结构都含有 String 和 Byte[],比方我置信 MemoryStream 外部必定有 Byte[],对吧,所以暂且放下状元和探花,看一下榜眼或者其余的简单类型。

如果你的眼睛犀利,你会发现 Free 的个数有 31W+,你必定想问这是什么意思?对,这表明以后托管堆上有 31W+ 的闲暇块,它的专业术语叫 碎片化,所以这条信息走漏出了以后托管堆有绝对重大的碎片化景象,接下来的问题就是为什么会这样?大多数状况呈现这种碎片化的起因在于托管堆上有很多的 pinned 对象,这种对象能够阻止 GC 在回收时对它的挪动,长此以往就会造成托管堆的四分五裂,所以找出这种景象对解决透露问题有很大的帮忙。

补充一下,这里能够借助 dotmemory,红色示意 pinned 对象,肉眼可见的大量的红色距离散布,最初的碎片率为 85%。

接下来的问题是如何找到这些 pinned 对象,其实在 CLR 中有一张 GCHandles 表,外面就记录了这些玩意。

3. 查看 GCHandles

要想找到所有的 pinned 对象,能够应用 !gchandles -stat 命令,简化输入如下:


||2:2:080> !gchandles -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffddbcc88a0      278        26688 System.Threading.Thread
00007ffddbcb47a8     1309       209440 System.RuntimeType+RuntimeTypeCache
00007ffddbcc7b38      100       348384 System.Object[]
00007ffddbc94b60     9359       673848 System.Reflection.Emit.DynamicResolver
00007ffddb5b7b98    25369      2841328 System.Threading.OverlappedData
Total 36566 objects

Handles:
    Strong Handles:       174
    Pinned Handles:       15
    Async Pinned Handles: 25369
    Ref Count Handles:    1
    Weak Long Handles:    10681
    Weak Short Handles:   326

从卦象中能够看出,以后有一栏为:Async Pinned Handles: 25369,这示意以后有 2.5w 的异步操作过程中被 pinned 住的对象,这个指标就相当不失常了,而且能够看出与 2.5W 的System.Threading.OverlappedData 一唱一和,有了这个思路,能够回过头来看一下托管堆,是否有绝对应的 2.5w 个相似封装过异步操作的简单类型对象?这里我再把 top10 Size 的托管堆列出来。


||2:2:080> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffd7e130ab8   116201     13014512 Newtonsoft.Json.Linq.JProperty
00007ffdd775e560    66176     16411648 System.Data.SqlClient._SqlMetaData
00007ffddbcc9da8    68808     17814644 System.Int32[]
00007ffddbcaf788    14140     21568488 System.String[]
00007ffddac72958    50256     22916736 System.Net.Sockets.SocketAsyncEventArgs
00007ffd7deb64b0      369     62115984 System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.ICustomAttributeProvider, mscorlib],[System.Type, mscorlib]][]
00007ffddbcc8610     8348    298313756 System.Char[]
00007ffddbcc74c0  1799807    489361500 System.String
000000000022e250   312151    855949918      Free
00007ffddbccc768   109156   1135674368 System.Byte[]

有了这种先入为主的思维,我想你必定发现了托管堆上的这个 50256 的 System.Net.Sockets.SocketAsyncEventArgs,看样子这回透露和 Socket 脱不了干系了,接下来能够查下这些 SocketAsyncEventArgs 到底被谁援用着?

4. 查看 SocketAsyncEventArgs 援用根

要想查看援用根,先从 SocketAsyncEventArgs 中导几个 address 进去。


||2:2:080> !dumpheap -mt 00007ffddac72958 0 0000000001000000
         Address               MT     Size
0000000000cc9dc0 00007ffddac72958      456     
0000000000ccc0d8 00007ffddac72958      456     
0000000000ccc358 00007ffddac72958      456     
0000000000cce670 00007ffddac72958      456     
0000000000cce8f0 00007ffddac72958      456     
0000000000cd0c08 00007ffddac72958      456     
0000000000cd0e88 00007ffddac72958      456     
0000000000cd31a0 00007ffddac72958      456     
0000000000cd3420 00007ffddac72958      456     
0000000000cd5738 00007ffddac72958      456     
0000000000cd59b8 00007ffddac72958      456     
0000000000cd7cd0 00007ffddac72958      456     

而后查看第一个和第二个 address 的援用根。


||2:2:080> !gcroot 0000000000cc9dc0
Thread 86e4:
    0000000018ecec20 00007ffd7dff06b4 xxxHttpServer.DaemonThread`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].DaemonThreadStart()
        rbp+10: 0000000018ececb0
            ->  000000000102e8c8 xxxHttpServer.DaemonThread`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  00000000010313a8 xxxHttpServer.xxxHttpRequestServer`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  000000000105b330 xxxHttpServer.HttpSocketTokenPool`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  000000000105b348 System.Collections.Generic.Stack`1[[xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]], xxxHttpServer]]
            ->  0000000010d36178 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]][]
            ->  0000000008c93588 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  0000000000cc9dc0 System.Net.Sockets.SocketAsyncEventArgs
||2:2:080> !gcroot 0000000000ccc0d8
Thread 86e4:
    0000000018ecec20 00007ffd7dff06b4 xxxHttpServer.DaemonThread`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].DaemonThreadStart()
        rbp+10: 0000000018ececb0
            ->  000000000102e8c8 xxxHttpServer.DaemonThread`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  00000000010313a8 xxxHttpServer.xxxHttpRequestServer`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  000000000105b330 xxxHttpServer.HttpSocketTokenPool`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  000000000105b348 System.Collections.Generic.Stack`1[[xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]], xxxHttpServer]]
            ->  0000000010d36178 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]][]
            ->  0000000000ccc080 xxxHttpServer.HttpSocketToken`2[[xxx.xxx, xxx],[xxx.RequestInfo, xxx]]
            ->  0000000000ccc0d8 System.Net.Sockets.SocketAsyncEventArgs

从输入信息看,貌似程序本人搭了一个 HttpServer,还搞了一个 HttpSocketTokenPool 池,好奇心来了,把这个类导出来看看怎么写的?

5. 寻找问题代码

还是老办法,应用 !savemodule 导出问题代码,而后应用 ILSpy 进行反编译。

说实话,这个 pool 封装的挺简陋的,既然 SocketAsyncEventArgs 有 5W+,我猜想这个 m_pool 池中预计也得好几万,为了验证思路,能够用 windbg 把它挖出来。

从图中的 size 能够看出,这个 pool 有大略 2.5w 的 HttpSocket,这就阐明这个所谓的 Socket Pool 其实并没有封装好。

三:总结

想本人封装一个 Pool,得要实现一些简单的逻辑,而不能仅仅是一个 PUSH 和 POP 就完事了。。。所以优化方向也很明确,想方法管制住这个 Pool,实现 Pool 该实现的成果。

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

退出移动版