【发布时间】:2022-01-13 12:23:28
【问题描述】:
我有 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 是这里的主要问题,指出它同时具有“具有直接委托根的已处置实例”(红色感叹号)和“已处置但仍未被 GC 处理的实例”(黄色感叹号)。分析器指示已被植根的代表是 AsyncCallbacks,源于 HttpWebRequest。
它也可能与 RemoteCertValidationCallback 相关,这与 HTTPS 证书验证有关,因为 TlsStream 是一个位于根目录下方的对象,即“已处置但未 GCed”。
考虑到所有这些 - 我怎样才能更正确地使用 HttpClient 并避免这些内存问题?我应该每小时左右强制一个GC.Collect()吗?我知道这被认为是不好的做法,但我不知道如何回收这个没有完全正确处理的内存,并且这些短期对象的更好使用模式对我来说并不明显,因为它似乎是 .NET 对象本身的缺陷。
更新
强制GC.Collect() 无效。
进程的总托管字节数最多保持在 20-30 MB 左右,而进程总内存(在任务管理器中)继续攀升,表明存在非托管内存泄漏。因此,这种使用模式会造成非托管内存泄漏。
我已尝试根据建议创建 HttpClient 和 HttpClientHandler 的类级别实例,但这没有明显的效果。即使我将这些设置为类级别,由于代理设置经常需要更改这一事实,它们仍然会重新创建并且很少重复使用。一旦发起请求,HttpClientHandler 不允许修改代理设置或任何属性,因此我不断地重新创建处理程序,就像最初使用独立的 using 语句所做的那样。
HttpClienthandler 仍然使用“直接委托根”处理 AsyncCallback -> HttpWebRequest。我开始怀疑 HttpClient 是否不是为快速请求和短期对象而设计的。看不到尽头..希望有人建议使用 HttpClientHandler 可行。
内存分析器截图:
【问题讨论】:
-
你为什么要在每次通话中处理
HttpClientHandler和HttpClient?HttpClient应该是整个应用程序中的一个长期存在的对象(因此,HttpClientHandler也应该如此。这样,您只需要生成一个实例。 -
很难看出发生了什么,但一般来说:找出根源。启用 .net 源代码调试并了解何时应该释放这些根,为什么?尝试删除
handler.CookieContainer = _CookieContainer- 也许这有什么可疑之处? -
我不确定,但如果我没记错的话,一般 http 不建议使用 using(),因为当您遇到错误时,它不会释放所有内存,您需要在连接上调用 abort。
-
@ChrisEelmaa 我在 prev repro 中使用了 void 而不是 Task,我的错 :) 实际上它没有泄漏,这是我的 repro gist.github.com/alexandrnikitin/86b3e5a517455f7ff8b0
标签: c# memory memory-leaks garbage-collection httpclient