zoukankan      html  css  js  c++  java
  • NET内存持续增长问题排查

    一、背景

          在某个NET程序的测试过程中,发现该程序的内存持续增长,无法释放,直到程序关闭后才能释放。经排查,确定问题的根源是在调用WCF服务的实现代码中,下面通过简单代码来重现问题发生的过程。
          1、服务端代码,只提供GetFile操作,返回相对较大的内容,便于快速看到内存持续增长的过程。
     1  class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             using (ServiceHost host = new ServiceHost(typeof(FileImp)))
     6             {
     7                 host.AddServiceEndpoint(typeof(IFile), new WSHttpBinding(), "http://127.0.0.1:9999/FileService");
     8                 if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null)
     9                 {
    10                     ServiceMetadataBehavior behavior = new ServiceMetadataBehavior();
    11                     behavior.HttpGetEnabled = true;
    12                     behavior.HttpGetUrl = new Uri("http://127.0.0.1:9999/FileService/metadata");
    13                     host.Description.Behaviors.Add(behavior);
    14                 }
    15                 host.Opened += delegate
    16                  {
    17                      Console.WriteLine("FileService已经启动,按任意键终止服务!");
    18                  };
    19                 host.Open();
    20                 Console.Read();
    21             }
    22         }
    23     }
    24 
    25     class FileImp : IFile
    26     {
    27         static byte[] _fileContent = new byte[1024 * 8];
    28 
    29         public byte[] GetFile(string fileName)
    30         {
    31             int loginID = OperationContext.Current.IncomingMessageHeaders.GetHeader<int>("LoginID", string.Empty);
    32             Console.WriteLine(string.Format("调用者ID:{0}", loginID));
    33             return _fileContent;
    34         }
    35     }

          2、客户端代码,循环调用GetFile操作,在调用前给消息头添加一些登录信息。另外为了避免垃圾回收机制执行的不确定性对内存增长的干扰,在每次调用完毕后,强制启动垃圾回收机制,对所有代进行垃圾回收,确保增长的内存都是可到达,无法对其进行回收。

     

     1  class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             int callCount = 0;
     6             int loginID = 0;
     7             while (true)
     8             {
     9                 using (ChannelFactory<IFile> channelFactory =
    10                     new ChannelFactory<IFile>(new WSHttpBinding(), "http://127.0.0.1:9999/FileService"))
    11                 {
    12                     IFile fileProxy = channelFactory.CreateChannel();
    13                     using (fileProxy as IDisposable)
    14                     {
    15                         //OperationContext.Current = new OperationContext(fileProxy as IContextChannel);
    16                         OperationContextScope scope = new OperationContextScope(fileProxy as IContextChannel);
    17                         var loginIDHeadInfo = MessageHeader.CreateHeader("LoginID", string.Empty, ++loginID);
    18                         OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo);
    19                         byte[] fileContent = fileProxy.GetFile(string.Empty);
    20                     }
    21                 }
    22                 GC.Collect();//强制启动垃圾回收
    23                 Console.WriteLine(string.Format("调用次数:{0}", ++callCount));
    24             }
    25         }
    26     }

    二、分析排查

          要解决内存持续增长的问题,首先需要定位问题,才能做相应的修复。对于逻辑简单的代码,可以简单直接通过排除法来定位问题代码所在,对于错综复杂的代码,就需要耗费一定时间了。当然除了排除法,还可以借助内存检测工具来快速定位问题代码。对于.net平台,微软提供.net辅助工具CLR Profiler帮助我们的性能测试人员以及研发人员,找到内存没有及时回收,占着内存不释放的方法。监测客户端程序运行的结果如下:
          从上图可看到OperationContextScope对象占用了98%的内存,当前OperationContextScope对象持有256个OperationContextScope对象的引用,这些OperationContextScope对象总共持有258个OperationContext的引用,每个OperationContext对象持有客户端代理的相关对象引用,导致每个客户端代理产生的内存在使用完毕后都无法得到释放。

    三、问题解决

          OperationContextScope类主要作用是创建一个块,其中 OperationContext 对象在范围之内。也就是说创建一个基于OperationContext 的上下文范围,在这范围内共享一个相同的OperationContext对象。这种上下文的特性是支持嵌套的,即一个大的上下文范围内可以有若干个小的上下文范围,而且不会造成相互不干扰。所以如果没显式调用该对象的Dispose方法结束当前上下文恢复前一上下文,再利用OperationContextScope类创建新的上下文,就会一直嵌套下去。所以在这里应该要显式调用Dispose方法结束当前OperationContextScope上下文范围,这样可以解决内存持续增长的问题了。
     1   class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             int callCount = 0;
     6             int loginID = 0;
     7             while (true)
     8             {
     9                 using (ChannelFactory<IFile> channelFactory =
    10                     new ChannelFactory<IFile>(new WSHttpBinding(), "http://127.0.0.1:9999/FileService"))
    11                 {
    12                     IFile fileProxy = channelFactory.CreateChannel();
    13                     using (fileProxy as IDisposable)
    14                     {
    15                         //OperationContext.Current = new OperationContext(fileProxy as IContextChannel);
    16                         using (OperationContextScope scope = new OperationContextScope(fileProxy as IContextChannel))
    17                         {
    18                             var loginIDHeadInfo = MessageHeader.CreateHeader("LoginID", string.Empty, ++loginID);
    19                             OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo);
    20                         }
    21                         byte[] fileContent = fileProxy.GetFile(string.Empty);
    22                     }
    23                 }
    24                 GC.Collect();//强制启动垃圾回收
    25                 Console.WriteLine(string.Format("调用次数:{0}", ++callCount));
    26             }
    27         }
    28     }

     四、问题根源

          OperationContextScope为什么能持有大量的OperationContext引用?从CLR Profiler工具获取的结果中可以看到OperationContextScope对象通过其内部OperationContextScope对象来持有大量OperationContext对象引用,可以推断该类应该有一个OperationContextScope类型的字段。下面看一下OperationContextScope类的源码。

     1  public sealed class OperationContextScope : IDisposable
     2     {
     3         [ThreadStatic]
     4         static OperationContextScope currentScope;
     5  
     6         OperationContext currentContext;
     7         bool disposed;
     8         readonly OperationContext originalContext = OperationContext.Current;
     9         readonly OperationContextScope originalScope = OperationContextScope.currentScope;
    10         readonly Thread thread = Thread.CurrentThread;
    11  
    12         public OperationContextScope(IContextChannel channel)
    13         {
    14             this.PushContext(new OperationContext(channel));
    15         }
    16  
    17         public OperationContextScope(OperationContext context)
    18         {
    19             this.PushContext(context);
    20         }
    21  
    22         public void Dispose()
    23         {
    24             if (!this.disposed)
    25             {
    26                 this.disposed = true;
    27                 this.PopContext();
    28             }
    29         }
    30  
    31         void PushContext(OperationContext context)
    32         {
    33             this.currentContext = context;
    34             OperationContextScope.currentScope = this;
    35             OperationContext.Current = this.currentContext;
    36         }
    37  
    38         void PopContext()
    39         {
    40             if (this.thread != Thread.CurrentThread)
    41                 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxInvalidContextScopeThread0)));
    42  
    43             if (OperationContextScope.currentScope != this)
    44                 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxInterleavedContextScopes0)));
    45  
    46             if (OperationContext.Current != this.currentContext)
    47                 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(SR.SFxContextModifiedInsideScope0)));
    48  
    49             OperationContextScope.currentScope = this.originalScope;
    50             OperationContext.Current = this.originalContext;
    51  
    52             if (this.currentContext != null)
    53                 this.currentContext.SetClientReply(null, false);
    54         }
    55     }

          当前的上下文对象由线程唯一的静态字段currentScope持有,其实例字段originalScope保持前一上下文对象的引用,如果使用完毕后不结束当前上下文范围,就会一直嵌套下去,导致所有OperationContext对象都保持可到达,垃圾回收机制无法进行回收,从而使得内存持续增长,直到内存溢出。

         

    五、总结

          类似OperationContextScope,TranscationScope以XXXScope结尾的类都可以看作Context+ContextScope的设计方式(参考Artech大神的博文:Context+ContextScope——这是否可以看作一种设计模式?),用于在同一范围内共享同一事物或对象。在使用这类上下文对象的时候,确保使用using关键字来使得上下文范围边界可控。

  • 相关阅读:
    利用搜狐查询接口举例说明
    超有用! 地址栏网址静默更新, 进入新网页也可以后退回去,.
    mouseenter 与 mouseover 区别于选择
    使用querySelector添加移除style和class
    网页修改<title ></title >标签内容
    (超实用)前端地址栏保存&获取参数,地址栏传输中文不在乱码
    html页面在苹果手机内,safari浏览器,微信中滑动不流畅问题解决方案
    python归一化方法
    opencv-python之投影
    matplotlib的用法
  • 原文地址:https://www.cnblogs.com/zhulehu/p/4658740.html
Copyright © 2011-2022 走看看