HttpClientHandler / HttpClient内存泄漏

我有10到150个长生命类对象,可以调用使用HttpClient执行简单HTTPS API调用的方法。 PUT调用示例:

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 Memory Profiler已经指出HttpClientHandler是这里的主要问题,声明它具有“具有直接委托根的Disposed实例”(红色感叹号)和“已经处置但仍然没有GCed的实例”(黄色感叹号)。 探查器指示已植根的委托是AsyncCallback ,源自HttpWebRequest。

它还可能与RemoteCertValidationCallback ,与HTTPS证书validation有关,因为TlsStream是根目录中的“Disposed但not not GCed”的对象。

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


UPDATE强制GC.Collect()无效。

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

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

HttpClienthandler仍然使用“直接委托根”来处理AsyncCallback – > HttpWebRequest。 我开始怀疑HttpClient是否不是为快速请求和短生命对象而设计的。 没有尽头……希望有人建议使用HttpClientHandler可行。


记忆探测器镜头: 初始堆栈指示HttpClientHandler是根本问题,具有应该已经GC的304个实时实例

在此处输入图像描述

在此处输入图像描述

使用reprforms的Alexandr Nikitin,我发现只有当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(); } } } 

这是我在不重新创建对象的情况下更改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; } } 

这是一个有效使用HttpClient和HttpClientHandler的基本Api客户端。 不要为每个请求重新创建HTTPClient。 尽可能多地重用Httpclient

我的性能Api客户端

 using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; //You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace MyApiClient { public class MyApiClient : IDisposable { private readonly TimeSpan _timeout; private HttpClient _httpClient; private HttpClientHandler _httpClientHandler; private readonly string _baseUrl; private const string ClientUserAgent = "my-api-client-v1"; private const string MediaTypeJson = "application/json"; public MyApiClient(string baseUrl, TimeSpan? timeout = null) { _baseUrl = NormalizeBaseUrl(baseUrl); _timeout = timeout ?? TimeSpan.FromSeconds(90); } public async Task PostAsync(string url, object input) { EnsureHttpClientCreated(); using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson)) { using (var response = await _httpClient.PostAsync(url, requestContent)) { response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } } public async Task PostAsync(string url, object input) where TResult : class, new() { var strResponse = await PostAsync(url, input); return JsonConvert.DeserializeObject(strResponse, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); } public async Task GetAsync(string url) where TResult : class, new() { var strResponse = await GetAsync(url); return JsonConvert.DeserializeObject(strResponse, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); } public async Task GetAsync(string url) { EnsureHttpClientCreated(); using (var response = await _httpClient.GetAsync(url)) { response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } public async Task PutAsync(string url, object input) { return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson)); } public async Task PutAsync(string url, HttpContent content) { EnsureHttpClientCreated(); using (var response = await _httpClient.PutAsync(url, content)) { response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } public async Task DeleteAsync(string url) { EnsureHttpClientCreated(); using (var response = await _httpClient.DeleteAsync(url)) { response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } public void Dispose() { _httpClientHandler?.Dispose(); _httpClient?.Dispose(); } private void CreateHttpClient() { _httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }; _httpClient = new HttpClient(_httpClientHandler, false) { Timeout = _timeout }; _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent); if (!string.IsNullOrWhiteSpace(_baseUrl)) { _httpClient.BaseAddress = new Uri(_baseUrl); } _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson)); } private void EnsureHttpClientCreated() { if (_httpClient == null) { CreateHttpClient(); } } private static string ConvertToJsonString(object obj) { if (obj == null) { return string.Empty; } return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); } private static string NormalizeBaseUrl(string url) { return url.EndsWith("/") ? url : url + "/"; } } } 

用法;

 using ( var client = new MyApiClient("http://localhost:8080")) { var response = client.GetAsync("api/users/findByUsername?username=alper").Result; var userResponse = client.GetAsync("api/users/findByUsername?username=alper").Result; } 

注意:如果您使用的是dependency injection库,请将MyApiClient注册为singleton。 为具体请求重用相同的对象是无状态且安全的。

正如Matt Clark所提到的,当您将其用作短期对象并为每个请求创建新的HttpClient时,默认的HttpClient泄漏。

作为一种解决方法,我能够通过使用以下Nuget包而不是内置的System.Net.Http程序集继续使用HttpClient作为短期对象: https ://www.nuget.org/packages/HttpClient

但是,不确定这个软件包的来源是什么,一旦我引用它,内存泄漏就消失了。 确保删除对内置.NET System.Net.Http库的引用,并使用Nuget包。