一、背景
在某个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;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
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));
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
二、分析排查
要解决内存持续增长的问题,首先需要定位问题,才能做相应的修复。对于逻辑简单的代码,可以简单直接通过排除法来定位问题代码所在,对于错综复杂的代 码,就需要耗费一定时间了。当然除了排除法,还可以借助内存检测工具来快速定位问题代码。对于.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));
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
四、问题根源
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);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
当前的上下文对象由线程***的静态字段currentScope持有,其实例字段originalScope保持前一上下文对象的引用,如果使用完毕后不 结束当前上下文范围,就会一直嵌套下去,导致所有OperationContext对象都保持可到达,垃圾回收机制无法进行回收,从而使得内存持续增长, 直到内存溢出。
五、总结
类似OperationContextScope,TranscationScope以XXXScope结尾的类都可以看作Context+ContextScope的设计方式(参考Artech大神的博文:Context+ContextScope——这是否可以看作一种设计模式?),用于在同一范围内共享同一事物或对象。在使用这类上下文对象的时候,确保使用using关键字来使得上下文范围边界可控。