内存泄漏问题分析之非托管资源泄漏

在某次巡查生产环境监控数据的时候,发现某个程序的内存占用偏高(大于500M)。对于这个程序的做用须要简单交代一下,这个程序是用作通信服务程序,经过Socket与IOT设备进行通信。由于了解这个程序的使用场景,因此对于该程序的内存占用偏高产生了怀疑。该程序服务的设备并很少,可是占用了几百兆的内存,很明显是存在问题的。html

对于该进程随后进行的分析也验证了这个想法,因为这个问题相对来讲比较典型,所以比较具备分享价值,经过对于该案例的分享但愿可让更多人了解和掌握内存泄漏问题分析的通常方法。数据库

内存泄漏问题分析的基本步骤

内存泄漏问题的分析能够分为三大部分:数组

  1. 确认问题
  2. 定位问题
  3. 解决问题

确认问题即确认内存确实存在泄漏问题,这个步骤不是光看看就能够,还须要尽可能的保留问题发生的现场。不论是什么样的内存泄漏问题,最好可以保留内存镜像用于分析(dump文件),由于内存泄漏问题有时候是瞬间的,若是不及时保留现场,等到有时间看的时候,可能程序已经恢复正常。保存内存镜像文件的时候最好能够间隔一段时间保留多个镜像文件用于对比分析,能够更好的定位问题。服务器

从windbg的角度分析问题

经过windbg扩展项sos,分析dump文件中的句柄和内存里面的对象类型。sos随着.net framework一块儿安装,能够适用于大多数状况下的调试。网络

首先检查内存中的对象统计信息,输入!dumpheap -statapp

0:000> !dumpheap -stat 
Statistics:
      MT    Count    TotalSize Class Name
……
6c3ab8d4      806        38688 System.RuntimeMethodInfoStub
6c363e90     2592        39424 System.RuntimeType[]
6b68105c     2265        45300 System.Net.SafeCloseSocket+InnerSafeCloseSocket
6b680f2c     2265        45300 System.Net.SafeNativeOverlapped
6c36d120      476        45696 System.Reflection.Emit.DynamicILGenerator
08eb8b40      334        49432 Newtonsoft.Json.Serialization.JsonProperty
6b671564     2264        54336 System.Net.Sockets.OverlappedCache
6c3a1dd8     1284        87312 System.Reflection.RuntimeParameterInfo
6c3a1d90     2092        92048 System.Signature
……
6c3a17a8     7179       114864 System.Int64
00d8a37c    10717       900228 ********.NetCommunicator.SocketConnectionInfo
6b674f28    10741       988172 System.Net.Sockets.Socket
6c35da78    88000      1056000 System.Object
08eb08c0    17403      1113792 Newtonsoft.Json.Linq.JProperty
082188ac    10717      1457512 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.String, mscorlib],[*******.RoadGate.API.Entity.MessagePacketModel, ***.RoadGate.API.Entity]][]
6c35e0e4      472     68400932 System.Char[]
6c35d6d8   126896    198551106 System.String
00af6ca0    34283    464007622      Free
6c361d04    25662   1037503288 System.Byte[]

获取到内存中对象的统计信息后重点关注堆栈中数量较多的类型,经过分析发现内存中有一万多个socket对象,还有一万多个放在ConcurrentDictionary中的业务自定义的实体类对象。因为当前分析的程序是通信服务器,socket的合理值很容易经过分析dump时刻的业务量获得结果(在本案例中确定是不合理的)。socket

通过咨询得知当前通信服务的通信对象远远达不到上万客户端的水平,所以很明显是socket相关的对象的处理出现了问题,出现了泄漏问题。对于.net程序来讲,socket相关对象属于非托管资源,非托管资源的使用原则上必须显式地进行释放或关闭操做。工具

对于应用建立的大多数对象,能够依赖 .NET 垃圾回收器来进行内存管理。 可是,若是建立包含非托管资源的对象,则当你使用完非托管资源后,必须显式释放这些资源。 最经常使用的非托管资源类型是包装操做系统资源的对象,如文件、窗口、网络链接或数据库链接。 虽然垃圾回收器能够跟踪封装非托管资源的对象的生存期,但没法了解如何发布并清理这些非托管资源。操作系统

虽然已经定位到通信服务对于socket的处理不当,可是非托管资源究竟是由于未能显示执行dispose方法致使的问题,仍是说这些对象一直被引用而没法被回收?想要对于非托管资源的问题进行详细分析,可使用!finalizequeue命令进行分析。该命令有三个可选参数:.net

  • -detail:显示须要清理的任何 SyncBlocks 的额外信息,以及有关等待清理的任何 RuntimeCallableWrappers (RCW) 的额外信息,这个选项也是默认值。
  • -allReady:选项显示全部准备终止的对象,不管它们已被垃圾回收标记成这样,仍是将被下一个垃圾回收标记。 “准备终止”列表中的对象为再也不为根的可终止对象。
  • -short:将输出限制为每一个对象的地址,能够跟-allReady或者-detail一块儿使用。

首先输入!finalizequeue -allready检查有多少能够回收的对象:

0:000> !finalizequeue -allready
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 71 finalizable objects (190ce568->190ce684)
generation 1 has 32 finalizable objects (190ce4e8->190ce568)
generation 2 has 56598 finalizable objects (19097090->190ce4e8)
Finalizable but not rooted:  976186dc 97619774 9761981c 97619844 9da920c8 9da920e0 9da9213c 9da92150 
……
Ready for finalization 0 objects (190ce684->190ce684)
Statistics for all finalizable objects that are no longer rooted:
      MT    Count    TotalSize Class Name
6c3711b8        1           16 System.Threading.Gen2GcCallback
6c36b328        2           24 System.Threading.TimerHolder
6b68c0fc        1           24 System.Net.Sockets.TcpClient
6b671fec        1           40 System.Net.Sockets.NetworkStream
6b68105c        8          160 System.Net.SafeCloseSocket+InnerSafeCloseSocket
6b6811a8        8          192 System.Net.SafeCloseSocket
6c35e2cc        4          208 System.Threading.Thread
67b788b8        4          208 System.Windows.Forms.Control+ThreadMethodEntry
00d8a37c        4          336 **************.NetCommunicator.SocketConnectionInfo
6b690e80        4          400 System.Net.Sockets.AcceptOverlappedAsyncResult
6b680f2c       20          400 System.Net.SafeNativeOverlapped
6c362a18       21          420 Microsoft.Win32.SafeHandles.SafeWaitHandle
6b671564       20          480 System.Net.Sockets.OverlappedCache
6b674f28        9          828 System.Net.Sockets.Socket
6c36207c      107         1284 System.WeakReference
6b671800       99         9900 System.Net.Sockets.OverlappedAsyncResult
Total 313 objects

从这个结果能够看到只有9个对象是没有根引用能够直接回收的,这说明其余的一万多个socket都是有root引用而形成内存没法释放。根引用是什么?在垃圾回收过程当中起到什么做用?

应用程序的根包含线程堆栈上的静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列。 每一个根或者引用托管堆中的对象,或者设置为空。

换言之,内存中众多的Socket对象就是被其余的变量引用了而没法释放。如何进一步查找这些对象的根引用呢?这须要借助!gcroot指令。GCRoot 命令将检查整个托管堆和句柄表以查找其余对象内的句柄和堆栈上的句柄。 而后,在每一个堆栈中搜索对象的指针,同时还搜索终结器队列。内存中有一万多个socket的对象,不须要所有去检查gcroot,只要看过一部分就会发现规律,在这些对象的gcroot的结果中有不少是相似的,最底层的引用关系是这样的:

->  029197f0 *******.RoadGate.TcpCommunicator.CameraTcpCommunictor
->  029199e4 *******.NetCommunicator.SocketConnectionInfoFactory
->  029199f0 System.Collections.Concurrent.ConcurrentDictionary`2[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]]
->  5e7c58f4 System.Collections.Concurrent.ConcurrentDictionary`2+Tables[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]]
->  80f55d48 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]][]
->  5e7afc40 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]]
->  02925630 System.Net.Sockets.Socket

如何解读这个引用关系呢?能够从下往上看,下层的对象是被上层的对象使用的。就当前这个对象来讲,首先被ConcurrentDictionary的Node内部类型包装,而且是ConcurrentDictionary内部的Node数组中的一员,该Node数组又被ConcurrentDictionary中的Tables内部类型再次封装,Tables内部类的对象直属于ConcurrentDictionary对象。该对象又是被SocketConnectionInfoFactory类对象使用。SocketConnectionInfoFactory是业务上定义的类型,若是要检查源代码,这个位置就是检查的入口。

既然ConcurrentDictionary中存放了大量的该释放而未被是否的对象,那么这个对象有多大呢?用!objsize来检查一下。

0:000> !objsize 029199f0
sizeof(029199f0) = 1141780680 (0x440e30c8) bytes (System.Collections.Concurrent.ConcurrentDictionary`2[[System.Net.Sockets.Socket, System],[IOT.NetCommunicator.SocketConnectionInfo, IOT.RoadGate.Communicator]])

经过windbg的统计,这一个对象存放的内容就占用了1G+的内存,跟抓取dump时的监控数据比较吻合。至此,内存泄漏的元凶就已经水落石出。

从代码角度分析问题

有了上一个章节内容的基础,再从代码角度分析出问题就比较容易了,代码中确实使用了多个ConcurrentDictionary保存了socket对象和一些业务对象的映射关系,可是对于设备断线重连的状况处理并不完善,致使重连后部分ConcurrentDictionary的内容获得了更新,而部分字典的内容并未被更新,并进而致使了内存泄漏的问题。

用伪代码描述设备上线和离网过程当中的相关逻辑:

//设备上线
if(不容许上线) return;
else
      建立Socket对象socket1;
      if(字典1中存在设备特征码ID)
            字典1[ID]=socket1;
      else
            字典1.Add(ID,socket1);
      字典2.Add(socket1,业务对象);

//设备离线
if(字典1中存在设备特征码ID)
      字典1.Remove(ID);

从伪代码中很容易看出来因为设备上线的时候往字典2中添加了内容可是设备离网以及设备建立重复链接的时候并无更新字典2中的内容致使了同一个设备会存在不少无用的socket对象。而这些对象没有业务上的意义并且还由于具备root而没法被清除。

总结

内存泄漏问题是后台服务中比较常见的一类故障,在发生内存泄漏事故时,若是单纯从服务运行场景的角度来分析每每得不到太好的效果并且耗时长而且难以找到准确的故障点。借助于windbg及sos插件的功能,综合使用gcrootdumpheapfinalizequeue等指令快速定位内存泄漏的准确位置,并在此基础上结合一些业务方面的知识和一些代码上的分析,就能够快速分析出内存泄漏的场景和缘由,并针对性的制定出相应的修复计划。

参考文献

相关文章
相关标签/搜索