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


应用程序的根包含线程堆栈上的静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列 。每个根或者引用托管堆中的对象,或者设置为空 。
换言之,内存中众多的Socket对象就是被其他的变量引用了而无法释放 。如何进一步查找这些对象的根引用呢?这需要借助 !gcroot指令 。GCRoot 命令将检查整个托管堆和句柄表以查找其他对象内的句柄和堆栈上的句柄 。然后,在每个堆栈中搜索对象的指针,同时还搜索终结器队列 。内存中有一万多个socket的对象,不需要全部去检查gcroot,只要看过一部分就会发现规律,在这些对象的gcroot的结果中有很多是类似的,最底层的引用关系是这样的:
  1. ->029197f0 *******.RoadGate.TcpCommunicator.CameraTcpCommunictor
  2. ->029199e4 *******.NetCommunicator.SocketConnectionInfoFactory
  3. ->029199f0 System.Collections.Concurrent.ConcurrentDictionary`2[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]]
  4. ->5e7c58f4 System.Collections.Concurrent.ConcurrentDictionary`2+Tables[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]]
  5. ->80f55d48 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]][]
  6. ->5e7afc40 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]]
  7. ->02925630 System.Net.Sockets.Socket

如何解读这个引用关系呢?可以从下往上看,下层的对象是被上层的对象使用的 。就当前这个对象来说,首先被ConcurrentDictionary的Node内部类型包装,并且是ConcurrentDictionary内部的Node数组中的一员,该Node数组又被ConcurrentDictionary中的Tables内部类型再次封装,Tables内部类的对象直属于ConcurrentDictionary对象 。该对象又是被 SocketConnectionInfoFactory类对象使用 。SocketConnectionInfoFactory是业务上定义的类型,如果要检查源代码,这个位置就是检查的入口 。
既然ConcurrentDictionary中存放了大量的该释放而未被是否的对象,那么这个对象有多大呢?可以使用 !objsize来查看 。
  1. 0:000> !objsize 029199f0
  2. sizeof(029199f0) = 1141780680 (0x440e30c8) bytes (System.Collections.Concurrent.ConcurrentDictionary`2[[System.Net.Sockets.Socket, System],[IOT.NetCommunicator.SocketConnectionInfo, IOT.RoadGate.Communicator]])
通过windbg的统计,这一个对象存放的内容就占用了一个多G的内存,跟抓取dump时的监控数据比较吻合 。至此,内存泄漏的元凶就已经水落石出 。
从代码角度分析问题有了上一个章节内容的基础,再从代码角度分析出问题就比较容易了,代码中确实使用了多个ConcurrentDictionary保存了socket对象和一些业务对象的映射关系,但是对于设备断线重连的情况处理并不完善,导致重连后部分ConcurrentDictionary的内容得到了更新,而部分字典的内容并未被更新,并进而导致了内存泄漏的问题 。
用伪代码描述设备上线和离网过程中的相关逻辑:
  1. //设备上线
  2. if(不允许上线) return;
  3. else
  4. 创建Socket对象socket1;
  5. if(字典1中存在设备特征码ID)
  6. 字典1[ID]=socket1;

    推荐阅读