zl程序教程

您现在的位置是:首页 >  .Net

当前栏目

NET内存持续增长问题排查

2023-03-09 22:01:14 时间

一、背景

      在某个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(nullfalse); 
  54.         } 
  55.     } 

 

 

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

     

五、总结

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