接上一篇文章垃圾回收 (GC) 在 .NET Core 中是如何工作的?-CSDN博客
GC 会分配堆段,其中每个段都是一系列连续的内存。 置于堆中的对象归类为 3 个代系之一:0、1 或 2。 代系可确定 GC 尝试在应用不再引用的托管对象上释放内存的频率。 编号较低的代系会更加频繁地进行 GC。
对象会基于其生存期从一个代系移到另一个代系。 随着对象生存期延长,它们会移到较高代系。 如前所述,较高代系进行 GC 的频率较低。 短期生存的对象始终保留在第 0 代中。 例如,在 Web 请求存在期间引用的对象的生存期较短。 应用程序级别单一实例通常会迁移到第 2 代。
当 ASP.NET Core 应用启动时,GC 会:
为初始堆段保留一些内存。
在运行时加载时提交一小部分内存。
HttpClient
未正确使用 HttpClient 可能会导致资源泄漏。 系统资源(如数据库连接、套接字、文件句柄等):
- 比内存更短缺。
- 在泄漏时出现的问题比内存更多。
经验丰富的 .NET 开发人员会对实现 IDisposable 的对象调用 Dispose。 未释放实现 IDisposable
的对象通常会导致内存泄漏或系统资源泄漏。
HttpClient
实现了 IDisposable
,但是不应在每次调用时进行释放。 而是应重用 HttpClient
。
下面的api会对每个请求创建并释放新的 HttpClient
实例:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{using (var httpClient = new HttpClient()){var result = await httpClient.GetAsync(url);return (int)result.StatusCode;}
}
上面的代码即使释放了 HttpClient
实例,实际网络连接也需要一些时间才能由操作系统释放。 持续创建新连接时,会发生端口耗尽。 每个客户端连接都需要自己的客户端端口。
防止端口耗尽的一种方法是重用同一个 HttpClient
实例:
private static readonly HttpClient _httpClient = new HttpClient();[HttpGet("httpclient2")]
public async Task<int> GetHttpClient2(string url)
{var result = await _httpClient.GetAsync(url);return (int)result.StatusCode;
}
HttpClient
实例会在应用停止时释放。
可以参阅以下内容,了解处理 HttpClient
实例的生存期:
- HttpClient 和生存期管理
- HTTPClient 工厂博客
对象池
上面的示例演示了如何将 HttpClient
实例设为静态,并由所有请求重用。 重用可防止资源耗尽。
对象池:
- 使用重用模式。
- 适用于创建成本高昂的对象。
池是预初始化对象的集合,这些对象可以在线程间保留和释放。 池可以定义分配规则,例如限制、预定义大小或增长速率。
下面的 API 会实例化 byte
缓冲区,该缓冲区对每个请求使用随机数字进行填充:
[HttpGet("array/{size}")]public byte[] GetArray(int size){var random = new Random();var array = new byte[size];random.NextBytes(array);return array;}
可以使用 ArrayPool<T> 创建 byte
缓冲区池,从而优化上面的代码。 静态实例可在请求间重用。
此方法的不同之处在于,会从 API 返回共用对象。 也就是说:
- 从方法返回后,对象会立即脱离控制。
- 无法释放对象。
若要设置对象的释放,请执行以下操作:
- 将共用数组封装在可释放对象中。
- 向 HttpContext.Response.RegisterForDispose 注册共用对象。
RegisterForDispose
会负责对目标对象调用 Dispose
,以便仅当 HTTP 请求完成时才会将其释放
private static ArrayPool<byte> _arrayPool = ArrayPool<byte>.Create();private class PooledArray : IDisposable
{public byte[] Array { get; private set; }public PooledArray(int size){Array = _arrayPool.Rent(size);}public void Dispose(){_arrayPool.Return(Array);}
}[HttpGet("pooledarray/{size}")]
public byte[] GetPooledArray(int size)
{var pooledArray = new PooledArray(size);var random = new Random();random.NextBytes(pooledArray.Array);HttpContext.Response.RegisterForDispose(pooledArray);return pooledArray.Array;
}
主要差异是分配的字节数,因而第 0 代回收数要少得多。