NET内存持续增长问题排查

开发 后端
在某个NET程序的测试过程中,发现该程序的内存持续增长,无法释放,直到程序关闭后才能释放。经排查,确定问题的根源是在调用WCF服务的实现代码中,下面通过简单代码来重现问题发生的过程。

一、背景

      在某个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(nullfalse); 
        } 
    } 
  • 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关键字来使得上下文范围边界可控。

 

 

责任编辑:王雪燕 来源: 博客园
相关推荐

2022-01-13 15:11:12

区块链数字化转型技术

2013-08-01 09:14:54

10GbE40GbESDN

2023-09-14 10:39:05

Google开发者

2012-02-01 08:40:07

Opera浏览器Opera Mini

2021-02-04 10:35:25

联想新基建数字化转型

2021-02-25 09:44:22

低代码GartnerForrester

2021-12-13 10:55:44

Mozilla财报收入增长

2010-03-01 15:34:19

IP呼叫中心

2010-01-27 16:08:05

SAS零售行业

2023-03-10 14:41:12

物联网IoT

2019-01-18 15:09:21

大数据IDC金融

2013-08-20 10:01:12

横向扩展架构IT趋势数据中心

2009-09-24 18:17:37

互联网域名域名系统威瑞信

2023-01-09 14:07:49

戴尔

2018-09-25 09:03:10

Windows 10微软

2017-04-26 12:03:32

短信

2015-02-01 15:18:51

微软Azure云服务

2018-04-26 22:36:05

物联网物联网平台支出

2020-12-01 10:43:02

云计算IT公有云

2009-09-09 21:05:45

安全网关以太网交换机Juniper
点赞
收藏

51CTO技术栈公众号