当前位置: 首页 > 科技观察

NET内存持续增长故障排除

时间:2023-03-16 20:04:26 科技观察

一、背景在测试一个NET程序时,发现程序内存持续增长,直到程序关闭才释放。经排查,问题根源出在调用WCF服务的实现代码上。下面是一个简单的代码来复现问题的过程。1、服务端代码只提供了GetFile操作,返回内容比较多,方便快速看到内存不断增长的过程。classProgram{staticvoidMain(string[]args){using(ServiceHosthost=newServiceHost(typeof(FileImp))){host.AddServiceEndpoint(typeof(IFile),newWSHttpBinding(),"http://127.0.0.1:9999/FileService");if(host.Description.Behaviors.Find()==null){ServiceMetadataBehaviorbehavior=newServiceMetadataBehavior();behavior.HttpGetEnabled=true;behavior.HttpGetUrl=newUri("http://127.0.0.1:9999/FileService/metadata");host.Description.Behaviors.Add(behavior);}host.Opened+=delegate{Console.WriteLine("FileService已经启动,按任意键终止服务!");};host.Open();控制台.Read();}}}classFileImp:IFile{staticbyte[]_fileContent=newbyte[1024*8];publicbyte[]GetFile(stringfileName){intloginID=OperationContext.Current.IncomingMessageHeaders.GetHeader("LoginID",string.Empty);Console.WriteLine(string.Format("CallerID:{0}",loginID));return_fileContent;}}2、客户端代码循环调用GetFile操作,添加一些login调用前的消息头信息。另外,为了避免垃圾回收机制执行的不确定性干扰内存增长,每次调用完成后,强制启动垃圾回收机制,对所有代进行垃圾回收,保证增加的内存可达且无法处理。回收。classProgram{staticvoidMain(string[]args){intcallCount=0;intloginID=0;while(true){using(ChannelFactorychannelFactory=newChannelFactory(newWSHttpBinding(),"http://127.0.0.1:9999/FileService")){IFilefileProxy=channelFactory.CreateChannel();using(fileProxyasIDisposable){//OperationContext.Current=newOperationContext(fileProxyasIContextChannel);OperationContextScope=newOperationContextScope(fileProxyasIContextChannel);varloginIDHeadInfo=LoginHeader.Create,++loginID);OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo);byte[]fileContent=fileProxy.GetFile(string.Empty);}}GC.Collect();//强制启动垃圾回收Console.WriteLine(string.Format("调用次数:{0}",++callCount));}}}二、分析与排查解决内存不断增长的问题,首先需要定位到问题所在,才能进行相应的修复。对于逻辑简单的代码,可以简单直接的使用排除法定位问题代码。对于复杂的代码,需要一定的时间。当然,除了排除法,还可以使用内存检测工具来快速定位问题代码。针对.net平台,微软提供了.net辅助工具CLRProfiler,帮助我们的性能测试人员和研发人员找到内存没有及时回收,内存没有释放的方法。客户端程序运行的监控结果如下:从上图可以看出OperationContextScope对象占用了98%的内存。当前的OperationContextScope对象持有256个对OperationContextScope对象的引用。这些OperationContextScope对象总共包含258个对OperationContext的引用。OperationContext对象保存着客户端代理的相关对象引用,使得每个客户端代理产生的内存在使用后无法释放。3.问题解决OperationContextScope类的主要功能是创建一个块,其中OperationContext对象在作用域内。也就是说,基于OperationContext创建一个context作用域,在这个作用域内共享同一个OperationContext对象。这个上下文的特点是支持嵌套,即在一个大上下文中可以有多个小上下文,互不干扰。因此,如果没有显式调用对象的Dispose方法结束当前上下文,恢复之前的上下文,再使用OperationContextScope类创建新的上下文,就会继续嵌套。所以这里应该显式调用Dispose方法结束当前OperationContextScope上下文,这样可以解决内存不断增长的问题。classProgram{staticvoidMain(string[]args){intcallCount=0;intloginID=0;while(true){using(ChannelFactorychannelFactory=newChannelFactory(newWSHttpBinding(),"http://127.0.0.1:9999/FileService")){IFilefileProxy=channelFactory.CreateChannel();使用(fileProxyasIDisposable){//OperationContext.Current=newOperationContext(fileProxyasIContextChannel);使用(OperationContextScope=newOperationContextScope(fileProxyasIContextChannel)){varloginID"HeadInfo=MessageIDHeader(string.Empty,++loginID);OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo);}byte[]fileContent=fileProxy.GetFile(string.Empty);}}GC.Collect();//强制启动垃圾回收Console.WriteLine(string.Format("Numberofcalls:{0}",++callCount));}}}四、问题根源,为什么OperationContextScope可以持有大量的OperationContext引用?从CLR得到的结果Profiler工具,可以看出OperationContextScope对象持有大量的OperationContext对象通过其内部的OperationContextScope对象引用,可以推断该类应该有一个OperationContextScopetypefield我们来看看OperationContextScope类的源码。publicsealedclassOperationContextScope:IDisposable{[ThreadStatic]staticOperationContextScopecurrentScope;OperationContextcurrentContext;booldisposed;readonlyOperationContextoriginalContext=OperationContext.Current;readonlyOperationContextScopeoriginalScope=OperationContextScope.currentScope;readonlyThreadthread=Thread.CurrentThread;publicOperationContextScope(IContextChannelOperation}Scope){this.PushContextpubliccontext(newOperation)上下文){this.PushContext(context);}publicvoidDispose(){if(!this.disposed){this.disposed=true;this.PopContext();}}voidPushContext(OperationContextcontext){this.currentContext=context;OperationContextScope.currentScope=this;OperationContext.Current=this.currentContext;}voidPopContext(){if(this.thread!=Thread.CurrentThread)throwDiagnosticUtility.ExceptionUtility.ThrowHelperError(newInvalidOperationException(SR.GetString(SR.SFxInvalidContextScopeThread0)));if(OperationContextScope.currentScope!=this)throwDiagnosticUtility.ExceptionUtility.ThrowHelperError(newInvalidOperationException(SR.GetString(SR.SFxInterleavedContextScopes0)));if(OperationContext.Current!=this.currentContext)throwDiagnosticUtility.ExceptionUtility.ThrowHelperError(newStringInp.Utility.ThrowHelperError(SFxContextModifiedInsideScope0)));OperationContextScope.currentScope=this.originalScope;OperationContext.Current=this.originalContext;if(this.currentContext!=null)this.currentContext.SetClientReply(null,false);}}当前上下文对象被线程使用***静态字段currentScope保存着它,它的实例字段originalScope保存着之前上下文对象的引用。如果当前上下文作用域在使用后没有结束,就会继续嵌套,导致所有的OperationContext对象都保持可达,垃圾回收机制无法回收,这样内存会一直增长,直到内存溢出。被视为设计模式?)用于在同一范围内共享相同的事物或对象。当使用这种类型的上下文对象时,请确保使用using关键字来保持上下文范围范围的可管理性。