生活随笔
收集整理的這篇文章主要介紹了
NET内存持续增长问题排查
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、背景
? ? 在某個NET程序的測試過程中,發現該程序的內存持續增長,無法釋放,直到程序關閉后才能釋放。經排查,確定問題的根源是在調用WCF服務的實現代碼中,下面通過簡單代碼來重現問題發生的過程。
? ??1、服務端代碼,只提供GetFile操作,返回相對較大的內容,便于快速看到內存持續增長的過程。
class?Program?????{?????????static?void?Main(string[]?args)?????????{?????????????using?(ServiceHost?host?=?new?ServiceHost(typeof(FileImp)))?????????????{?????????????????host.AddServiceEndpoint(typeof(IFile),?new?WSHttpBinding(),?"http://127.0.0.1:9999/FileService");?????????????????if?(host.Description.Behaviors.Find<ServiceMetadataBehavior>()?==?null)?????????????????{?????????????????????ServiceMetadataBehavior?behavior?=?new?ServiceMetadataBehavior();?????????????????????behavior.HttpGetEnabled?=?true;?????????????????????behavior.HttpGetUrl?=?new?Uri("http://127.0.0.1:9999/FileService/metadata");?????????????????????host.Description.Behaviors.Add(behavior);?????????????????}?????????????????host.Opened?+=?delegate??????????????????{??????????????????????Console.WriteLine("FileService已經啟動,按任意鍵終止服務!");??????????????????};?????????????????host.Open();?????????????????Console.Read();?????????????}?????????}?????}??????class?FileImp?:?IFile?????{?????????static?byte[]?_fileContent?=?new?byte[1024?*?8];??????????public?byte[]?GetFile(string?fileName)?????????{?????????????int?loginID?=?OperationContext.Current.IncomingMessageHeaders.GetHeader<int>("LoginID",?string.Empty);?????????????Console.WriteLine(string.Format("調用者ID:{0}",?loginID));?????????????return?_fileContent;?????????}?????}?? ? 2、客戶端代碼,循環調用GetFile操作,在調用前給消息頭添加一些登錄信息。另外為了避免垃圾回收機制執行的不確定性對內存增長的干擾,在每次調用完畢后,強制啟動垃圾回收機制,對所有代進行垃圾回收,確保增長的內存都是可到達,無法對其進行回收。
class?Program?????{?????????static?void?Main(string[]?args)?????????{?????????????int?callCount?=?0;?????????????int?loginID?=?0;?????????????while?(true)?????????????{?????????????????using?(ChannelFactory<IFile>?channelFactory?=?????????????????????new?ChannelFactory<IFile>(new?WSHttpBinding(),?"http://127.0.0.1:9999/FileService"))?????????????????{?????????????????????IFile?fileProxy?=?channelFactory.CreateChannel();?????????????????????using?(fileProxy?as?IDisposable)?????????????????????{??????????????????????????????????????????????????OperationContextScope?scope?=?new?OperationContextScope(fileProxy?as?IContextChannel);?????????????????????????var?loginIDHeadInfo?=?MessageHeader.CreateHeader("LoginID",?string.Empty,?++loginID);?????????????????????????OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo);?????????????????????????byte[]?fileContent?=?fileProxy.GetFile(string.Empty);?????????????????????}?????????????????}?????????????????GC.Collect();?????????????????Console.WriteLine(string.Format("調用次數:{0}",?++callCount));?????????????}?????????}?????}?二、分析排查
? ??要解決內存持續增長的問題,首先需要定位問題,才能做相應的修復。對于邏輯簡單的代碼,可以簡單直接通過排除法來定位問題代碼所在,對于錯綜復雜的代 碼,就需要耗費一定時間了。當然除了排除法,還可以借助內存檢測工具來快速定位問題代碼。對于.net平臺,微軟提供.net輔助工具CLR Profiler幫助我們的性能測試人員以及研發人員,找到內存沒有及時回收,占著內存不釋放的方法。監測客戶端程序運行的結果如下:
? ? 從上圖可看到OperationContextScope對象占用了98%的內存,當前OperationContextScope對象持有256個 OperationContextScope對象的引用,這些OperationContextScope對象總共持有258個 OperationContext的引用,每個OperationContext對象持有客戶端代理的相關對象引用,導致每個客戶端代理產生的內存在使用 完畢后都無法得到釋放。
三、問題解決
? ??OperationContextScope類主要作用是創建一個塊,其中 OperationContext 對象在范圍之內。也就是說創建一個基于OperationContext?的上下文范圍,在這范圍內共享一個相同的OperationContext對 象。這種上下文的特性是支持嵌套的,即一個大的上下文范圍內可以有若干個小的上下文范圍,而且不會造成相互不干擾。所以如果沒顯式調用該對象的 Dispose方法結束當前上下文恢復前一上下文,再利用OperationContextScope類創建新的上下文,就會一直嵌套下去。所以在這里應 該要顯式調用Dispose方法結束當前OperationContextScope上下文范圍,這樣可以解決內存持續增長的問題了。
class?Program?????{?????????static?void?Main(string[]?args)?????????{?????????????int?callCount?=?0;?????????????int?loginID?=?0;?????????????while?(true)?????????????{?????????????????using?(ChannelFactory<IFile>?channelFactory?=?????????????????????new?ChannelFactory<IFile>(new?WSHttpBinding(),?"http://127.0.0.1:9999/FileService"))?????????????????{?????????????????????IFile?fileProxy?=?channelFactory.CreateChannel();?????????????????????using?(fileProxy?as?IDisposable)?????????????????????{??????????????????????????????????????????????????using?(OperationContextScope?scope?=?new?OperationContextScope(fileProxy?as?IContextChannel))?????????????????????????{?????????????????????????????var?loginIDHeadInfo?=?MessageHeader.CreateHeader("LoginID",?string.Empty,?++loginID);?????????????????????????????OperationContext.Current.OutgoingMessageHeaders.Add(loginIDHeadInfo);?????????????????????????}?????????????????????????byte[]?fileContent?=?fileProxy.GetFile(string.Empty);?????????????????????}?????????????????}?????????????????GC.Collect();?????????????????Console.WriteLine(string.Format("調用次數:{0}",?++callCount));?????????????}?????????}?????}??四、問題根源
? ? OperationContextScope為什么能持有大量的OperationContext引用?從CLR Profiler工具獲取的結果中可以看到OperationContextScope對象通過其內部OperationContextScope對象來 持有大量OperationContext對象引用,可以推斷該類應該有一個OperationContextScope類型的字段。下面看一下OperationContextScope類的源碼。
public?sealed?class?OperationContextScope?:?IDisposable?????{?????????[ThreadStatic]?????????static?OperationContextScope?currentScope;???????????OperationContext?currentContext;?????????bool?disposed;?????????readonly?OperationContext?originalContext?=?OperationContext.Current;?????????readonly?OperationContextScope?originalScope?=?OperationContextScope.currentScope;?????????readonly?Thread?thread?=?Thread.CurrentThread;???????????public?OperationContextScope(IContextChannel?channel)?????????{?????????????this.PushContext(new?OperationContext(channel));?????????}???????????public?OperationContextScope(OperationContext?context)?????????{?????????????this.PushContext(context);?????????}???????????public?void?Dispose()?????????{?????????????if?(!this.disposed)?????????????{?????????????????this.disposed?=?true;?????????????????this.PopContext();?????????????}?????????}???????????void?PushContext(OperationContext?context)?????????{?????????????this.currentContext?=?context;?????????????OperationContextScope.currentScope?=?this;?????????????OperationContext.Current?=?this.currentContext;?????????}???????????void?PopContext()?????????{?????????????if?(this.thread?!=?Thread.CurrentThread)?????????????????throw?DiagnosticUtility.ExceptionUtility.ThrowHelperError(new?InvalidOperationException(SR.GetString(SR.SFxInvalidContextScopeThread0)));???????????????if?(OperationContextScope.currentScope?!=?this)?????????????????throw?DiagnosticUtility.ExceptionUtility.ThrowHelperError(new?InvalidOperationException(SR.GetString(SR.SFxInterleavedContextScopes0)));???????????????if?(OperationContext.Current?!=?this.currentContext)?????????????????throw?DiagnosticUtility.ExceptionUtility.ThrowHelperError(new?InvalidOperationException(SR.GetString(SR.SFxContextModifiedInsideScope0)));???????????????OperationContextScope.currentScope?=?this.originalScope;?????????????OperationContext.Current?=?this.originalContext;???????????????if?(this.currentContext?!=?null)?????????????????this.currentContext.SetClientReply(null,?false);?????????}?????}?? ? 當前的上下文對象由線程唯一的靜態字段currentScope持有,其實例字段originalScope保持前一上下文對象的引用,如果使用完畢后不 結束當前上下文范圍,就會一直嵌套下去,導致所有OperationContext對象都保持可到達,垃圾回收機制無法進行回收,從而使得內存持續增長, 直到內存溢出。
? ?
五、總結
? ? 類似OperationContextScope,TranscationScope以XXXScope結尾的類都可以看作Context+ContextScope的設計方式(參考Artech大神的博文:Context+ContextScope——這是否可以看作一種設計模式?),用于在同一范圍內共享同一事物或對象。在使用這類上下文對象的時候,確保使用using關鍵字來使得上下文范圍邊界可控。
作者:豬樂乎
來源:51CTO
總結
以上是生活随笔為你收集整理的NET内存持续增长问题排查的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。