2015-01-01 98 views
17

我有任何地方从10-150长的生活类对象调用使用HttpClient执行简单的HTTPS API调用的方法。一个PUT调用示例:HttpClientHandler/HttpClient内存泄漏

using (HttpClientHandler handler = new HttpClientHandler()) 
{ 
    handler.UseCookies = true; 
    handler.CookieContainer = _Cookies; 

    using (HttpClient client = new HttpClient(handler, true)) 
    { 
     client.Timeout = new TimeSpan(0, 0, (int)(SettingsData.Values.ProxyTimeout * 1.5)); 
     client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Statics.UserAgent); 

     try 
     { 
      using (StringContent sData = new StringContent(data, Encoding.UTF8, contentType)) 
      using (HttpResponseMessage response = await client.PutAsync(url, sData)) 
      { 
       using (var content = response.Content) 
       { 
        ret = await content.ReadAsStringAsync(); 
       } 

      } 
     } 
     catch (ThreadAbortException) 
     { 
      throw; 
     } 
     catch (Exception ex) 
     { 
      LastErrorText = ex.Message; 
     } 
    } 
} 

2-3小时后运行这些方法,其中包括通过using陈述妥善处置,该计划已穆斯特1GB的内存,1.5GB,并最终以各种崩溃的内存不足错误。很多时候,连接都是通过不可靠的代理服务器进行的,因此连接可能无法按预期完成(超时和其他错误很常见)。

.NET内存事件探查器指出HttpClientHandler是这里的主要问题,指出它具有'直接代表根'的废弃实例(红色感叹号)和'已处理但仍未GCed的实例'(黄色感叹号)。探查器指示的代表已根据AsyncCallback s,源自HttpWebRequest。

它也可能与RemoteCertValidationCallback有关,它与HTTPS证书验证有关,因为TlsStream是“Disposed but not GCed”根目录下的一个对象。

考虑到这一切 - 我如何更正确地使用HttpClient并避免这些内存问题?我应该每小时强制一次GC.Collect()吗?我知道这被认为是不好的做法,但我不知道如何回收这个不太适合处理的内存,并且对于这些短期对象来说更好的使用模式对我来说并不明显,因为它似乎成为.NET对象本身的一个缺陷。


UPDATE 强制GC.Collect()没有效果。

进程的托管字节总数最多保持20-30 MB左右,而进程整体内存(在任务管理器中)持续攀升,表明存在非托管内存泄漏。因此,这种使用模式正在创建一个非托管内存泄漏。

我已经尝试创建HttpClient和HttpClientHandler的每个建议的类级别的实例,但是这没有可观的效果。即使我将它们设置为课程级别,由于代理设置通常需要更改,因此它们仍会重新创建并很少重复使用。一旦请求被启动,HttpClientHandler不允许修改代理设置或任何属性,所以我不断地重新创建处理程序,就像最初对独立的using声明所做的那样。

HttpClienthandler仍然与“直接委托根”处置为AsyncCallback - > HttpWebRequest。我开始怀疑,也许HttpClient不是专为快速请求和短期生活对象而设计的。没有尽头,希望有人建议使用HttpClientHandler可行。


存储器剖析镜头: Initial stack indicating that HttpClientHandler is the root issue, having 304 live instances that should have been GC'd

enter image description here

enter image description here

+0

你为什么要在每次调用中放置'HttpClientHandler'和'HttpClient'? 'HttpClient'应该是整个应用程序中的一个长寿命对象(因此,'HttpClientHandler'也应该是这样的,这样,你只需要生成一个实例。 –

+0

我剪掉了代码中不重要的部分,但头部根据请求而波动,并且代理会根据连接成功/失败而改变,HttpClientHandler对象的大量维护都会发生,因此我认为每次只重新创建一次会比较简单,但它仍然不能满足问题为什么这些对象不能重复创建而不会重复创建 – user1111380

+0

如果确实尝试了GC.collect(),您是否看到正在收集这些TlsStream对象? – zaitsman

回答

13

使用摄制形式亚历山大·尼基京,我能发现,这似乎只发生时,你有HttpClient的是一个短暂的物体。如果您的处理程序和客户端长寿这似乎并没有发生:

using System; 
using System.Net.Http; 
using System.Threading.Tasks; 

namespace HttpClientMemoryLeak 
{ 
    using System.Net; 
    using System.Threading; 

    class Program 
    { 
     static HttpClientHandler handler = new HttpClientHandler(); 

     private static HttpClient client = new HttpClient(handler); 

     public static async Task TestMethod() 
     { 
      try 
      { 
       using (var response = await client.PutAsync("http://localhost/any/url", null)) 
       { 
       } 
      } 
      catch 
      { 
      } 
     } 

     static void Main(string[] args) 
     { 
      for (int i = 0; i < 1000000; i++) 
      { 
       Thread.Sleep(10); 
       TestMethod(); 
      } 

      Console.WriteLine("Finished!"); 
      Console.ReadKey(); 
     } 
    } 
} 
+2

感谢您接近这一点。不幸的是,HttpClient类当时不符合我的要求 - 由于公共代理的动态性和不稳定性,对象必须经常重新创建。看起来HttpClient对于短连接的连接并不是一个可行的解决方案 - 更改代理设置需要重新构建HttpClientHandler,因此需要重新构建HttpClient。无论哪种方式,物体应该能够根据需要长时间或短时间存在而不会泄漏;这绝对是HttpClient中的一个缺陷。 – user1111380

0

马特·克拉克提到的,默认HttpClient泄漏,当您使用它作为一个短暂的对象,并创建每个请求的新HttpClients。

作为一种变通方法,我能够使用的HttpClient作为一个短暂的目的是通过使用下面的NuGet包,而不是保持内置System.Net.Http装配: https://www.nuget.org/packages/HttpClient

不知道这是什么包的起源但是,一旦我引用它,内存泄漏消失了。确保您删除对内置.NET System.Net.Http库的引用,并改为使用Nuget包。

+0

不幸的是,似乎所有者未列出此软件包“所有者未列出此软件包,这可能意味着软件包已过时或不应再使用。” –

+0

即使未列出,您仍然可以使用它。它仍然有效。 –

0

这是我如何更改HttpClientHandler代理而不重新创建对象。

public static void ChangeProxy(this HttpClientHandler handler, WebProxy newProxy) 
{ 
    if (handler.Proxy is WebProxy currentHandlerProxy) 
    { 
     currentHandlerProxy.Address = newProxy.Address; 
     currentHandlerProxy.Credentials = newProxy.Credentials; 
    } 
    else 
    { 
     handler.Proxy = newProxy; 
    } 
}