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)
- {
- //OperationContext.Current = new OperationContext(fileProxy as IContextChannel);
- 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)
- {
- //OperationContext.Current = new OperationContext(fileProxy as IContextChannel);
- 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關鍵字來使得上下文范圍邊界可控。