C#使用HttpClient多并发HttpGet请求后内存未释放问题
1. HttpClient实例滥用
如果每次请求都新建HttpClient实例,会导致底层HttpMessageHandler和套接字资源无法及时回收。默认HttpClient的HttpMessageHandler自带连接池,频繁创建实例会造成资源泄漏,累积大量未释放的句柄和内存。正确做法是复用单例HttpClient,或者用IHttpClientFactory管理实例生命周期。
2. 异步任务管理不当
- 并发请求如果没配置
CancellationToken,或者任务未被正确等待/终止,可能导致任务实例和关联资源长期驻留内存。比如用Task.WhenAll却未处理异常,部分任务处于挂起状态,GC无法回收这些任务对象。 - 若使用
async void方法,未捕获的异常会导致线程池资源泄漏,同时任务状态无法被正确清理。
3. 大对象堆(LOH)碎片
批量请求返回的响应内容(即使仅状态码,也可能包含响应头、缓存数据)若为超过85KB的大对象,会被分配到LOH。LOH回收频率低且不会压缩,多次扫描后会产生碎片,导致内存无法有效复用,表现为内存持续占用。比如缓存了每个请求的响应内容,或未及时丢弃响应流,都会累积大量LOH对象。
4. 静态/全局集合未清理
如果用静态列表、字典存储扫描结果,每次扫描后未清空这些集合,或集合内对象被其他全局引用持有,GC无法回收这些对象。比如静态变量List<ScanResult>每次扫描都追加数据却不清理,内存自然持续增长。
5. HTTP资源未释放
手动处理HttpWebRequest或HttpResponseMessage时,若未用using语句包裹或调用Dispose(),会导致响应流、连接资源无法释放。比如获取HttpResponseMessage后,未释放Content或整个响应对象,这些资源会被持续持有。
错误示例:
var response = client.GetAsync(url).Result; // 未调用 response.Dispose()
正确示例:
using var response = await client.GetAsync(url);
6. GC回收时机偏差
有时内存占用高不一定是泄漏,可能是GC尚未触发回收。但多次扫描后内存持续上涨且不回落,大概率是泄漏。可在扫描完成后手动调用GC.Collect()测试,若内存仍未下降,说明存在未释放的资源。另外,.NET内存回收分代,老年代对象若被引用,会长期占用内存。
内容的提问来源于stack exchange,提问作者devLander

