.Net调试内存泄漏不断增长小记——SocketAsyncEventArgs

来源:互联网 发布:中国时间服务器域名 编辑:程序博客网 时间:2024/06/12 22:07

现象

用C#异步方式实现的网络底层协议,开发的服务器。上线运行一段时间后,发现一开始内存非常稳定,但是过了一定时间后,内存使用量会开始不停的上涨。直到内存耗尽。

排查

遇到这一问题可以明确的是内存发生了泄漏。由于.Net中,托管对象的内存是由垃圾回收机制负责回收的。所以存在内存增长的情况,往往不是因为没有释放。而是有几种原因

  1. 分配的内存,比垃圾回收的还要快
  2. 对象存在引用,没有办法被垃圾回收机制回收。

对于第一种情况,我仔细排查了代码中使用new分配内存的位置,往往这种情况,是因为在一个逻辑循环中不断分配内存,并且该循环占用了大量的CPU,导致其他线程(GC)没有空隙执行释放造成的。常见的是在一些线程函数中的,无限循环(业务逻辑的帧循环)中执行了new。

第二种情况则是我们以为我们分配的对象在生命周期结束后,不会再被其他对象引用,而应该被GC回收,但实际情况是因为某些原因(比如,函数返回,输出参数,将对应的引用返回到其生命周期外部),对象实际被其他对象使用了,导致GC不认为该对象是死对象,从而无法回收。这种情况比较多见,往往是编写代码的疏忽。

但针对以上两种情况进行排查后,我发现我们的代码都没有这种问题,因为对这个问题的排查陷入一个无解的情况。

由于在进程中,执行new的地方,除了业务逻辑,只剩下网络底层协议。因为把目光转向了网络底层实现。在我们的网络底层实现中,使用new的只有对网络消息的封包,和网络异步操作时的SocketAsyncEventArgs的使用。根据内存增长的速度,我们得出大概每秒会上涨接近200KB的内存,而我们测试时的峰值人数才100人,也就是平均每人每秒,在基本和网络只保持心跳的情况下,都要发出去2000字节,而我们的心跳包大概也就2字节。所以基本排除了是消息封包的内存出现问题。

那么接下来就是SocketAsyncEventArgs的分配了,仔细检查代码,这个东西我们是作为局部变量使用的,每次都是重新分配,理应不会产生上述两种情况。所以看似又进入了死胡同。抱着试试看的态度,去网上查了下,看到.net 中异步SOCKET发送数据时碰到的内存问题这篇文章(这里是我转载的),发现里面的问题和我们的非常类似,然后抱着试试的想法,对SocketAsyncEventArgs对象执行了SetBuffer(null, 0, 0),调试,问题解决。

原因

问题解决后,发现原来产生问题的原因是这样的,按.net 中异步SOCKET发送数据时碰到的内存问题中的实验来看,是因为,没有执行过SetBuffer(null, 0, 0)的话,先前在我们进行异步收发的时候SetBuffer()有设置过一个byte[]对象,而导致SocketAsyncEventArgs一直引用这个byte[],使byte[]无法释放,产生内存增长。文章中最后总结为两个死对象互相引用,从而导致GC无法确定该释放哪个。从而使两个对象都无法被回收。

虽然我按他的方法试验后,确实解决了问题。但我其实对这个原因还是存在一定的疑问。首先按照GC的回收机制,只有当从root对象中无法到达的对象才会被认定为死对象。那么在这个问题中,由于没有执行SetBuffer(null, 0, 0),那么byte[]始终被SocketAsyncEventArgs引用,那么如果SocketAsyncEventArgs是可达的话,byte[]才会可达,而SocketAsyncEventArgs作为局部变量,理应在函数执行完后,成为一个死对象,连带其中的byte[]都成为不可达的,而byte[]并不可能引用SocketAsyncEventArgs,也就是不存在两个死对象互相引用的问题。这也是我一直在开始并没有怀疑这个new分配的原因。

其实真正的原因并不是两个死对象互相引用,而是在于非托管资源与托管资源的释放不同。SocketAsyncEventArgs具有Dispose接口,加之其和网络相关,因此其内部很可能存在非托管的资源,因此不调用Dispose的话是无法释放的。而byte[]如果被SocketAsyncEventArgs引用,而SocketAsyncEventArgs没有释放,那么byte[]自然也无法释放了。

在转载文章的代码中,将SetBuffer(null,0,0)修改成Dispose调用,同样解决问题

class Program{   public class simpleRef   {       public byte[] byteref;       public void SetBuffer(byte[] buf)       {           byteref = buf;       }   }   static void Main(string[] args)   {       Console.WriteLine("Press any key to start.");       Console.ReadKey();       for (int i = 0; i < 100; ++i)       {           for(int j = 0; j < 10000; ++j)           {               var e = new SocketAsyncEventArgs();               //e.SetBuffer(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, 0, 0);               //e.SetBuffer(null, 0, 0);               //e.Dispose();           }           System.Threading.Thread.Sleep(1);       }       //}       Console.WriteLine("Over!!!!!!!!!!!");       Console.ReadLine();   }}

依次打开代码中注释的三行代码测试,当不做任何处理时,内存峰值稳定在715752kb
使用转载文章的方法,SetBuffer(null, 0, 0),内存峰值稳定在13120kb
使用Dispose,内存峰值稳定在7980kb

可以看到,在循环中,我们分配的byte,即便完全没有释放,也只有10000kb左右,由此可见,真正占内存的资源是SocketAsyncEventArgs。SetBuffer这个函数会产生大量的内存消耗,其中应该包括大量非托管的资源。虽然SetBuffer(null, 0, 0)执行后,确实能释放掉一部分资源,但显然没有释放干净,只有使用Dispose,才是正确的释放方式

经过这个问题,老实说也提醒我平时要注意对于托管资源和非托管资源的释放,平时太依赖GC,几乎不太注意这个问题,看来也不是好习惯,敲响了警钟

原创粉丝点击