垃圾回收 (GC) 在 .NET Core 中的工作方式
GC 分配堆段,其中每个段都是一系列连续的内存。 位于堆中的对象归类为三个代之一:0、1或2。 该代确定 GC 尝试释放应用程序不再引用的托管对象上内存的频率。 较低编号的生成更为频繁。
根据对象的生存期,将对象从一代移到另一代。 随着对象的运行时间较长,它们会移到较高的代中。 如前所述,较高的版本是不太常见的垃圾回收。 短期生存期的对象始终保留在第0代中。 例如,在 web 请求过程中引用的对象的生存期很短。 应用程序级别 单一实例 通常迁移到第2代。
当 ASP.NET Core 应用启动时,GC:
出于性能方面的原因,上述内存分配已完成。 性能优势来自连续内存中的堆段。
调用 GC。收集
调用 GC。显式收集 :
分析应用的内存使用情况
专用工具可帮助分析内存使用量:
使用以下工具分析内存使用量:
检测内存问题
任务管理器可用于了解 ASP.NET 应用使用的内存量。 任务管理器内存值:
如果任务管理器内存值无限增加且从未平展,则应用程序的内存泄漏。 以下部分演示并解释了几种内存使用模式。
示例显示内存使用情况应用
GitHub 上提供了MemoryLeak 示例应用。 MemoryLeak 应用:
运行 MemoryLeak。 分配的内存缓慢增加,直到 GC 发生。 内存增加是因为该工具分配自定义对象来捕获数据。 下图显示了 Gen 0 GC 发生时的 MemoryLeak 索引页。 此图表显示 0 RPS (每秒的请求数) ,因为尚未调用 API 控制器中的 API 终结点。
此图表显示内存使用量的两个值:
暂时性对象
以下 API 创建一个 10 KB 的字符串实例,并将其返回给客户端。 对于每个请求,将在内存中分配一个新的对象,并将其写入响应中。 字符串作为 UTF-16 字符存储在 .NET 中,因此每个字符需要2个字节的内存。
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
下面的关系图是使用相对较小的负载生成的,用于显示 GC 如何影响内存分配。
上面的图表显示:
以下图表采用可由计算机处理的最大吞吐量。
上面的图表显示:
工作站 GC 与服务器 GC
.NET 垃圾回收器具有两种不同的模式:
GC 模式可以在项目文件中或在发布的应用程序的 runtimeconfig.js 文件中显式设置。 以下标记显示 ServerGarbageCollection 项目文件中的设置:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
更改 ServerGarbageCollection 项目文件需要重新生成应用。
注意: 服务器垃圾回收在具有单个核心的计算机上 不可用。 有关详细信息,请参阅 IsServerGC。
下图显示了使用工作站 GC 的占用大量 RPS 的内存配置文件。
此图表与服务器版本之间的区别非常重要:
在典型的 web 服务器环境中,CPU 使用率比内存更重要,因此服务器 GC 更好。 如果内存使用率很高且 CPU 使用率相对较低,则工作站 GC 可能会更高的性能。 例如,在内存不足的情况下承载几个 web 应用的高密度。
使用 Docker 和小型容器的 GC
当在一台计算机上运行多个容器化应用程序时,工作站 GC 可能比服务器 GC 更 preformant。 有关详细信息,请参阅在小型 容器中运行服务器 gc 和 在小型容器方案中运行服务器 GC 部分– GC 堆的硬限制。
持久性对象引用
GC 无法释放所引用的对象。 引用但不再需要的对象将导致内存泄露。 如果应用经常分配对象,但在不再需要对象之后无法释放它们,则内存使用量将随着时间的推移而增加。
以下 API 创建一个 10 KB 的字符串实例,并将其返回给客户端。 与上一示例的不同之处在于,此实例由静态成员引用,这意味着它不能用于收集。
private static ConcurrentBag<string> _staticStrings = new ConcurrentBag<string>();[HttpGet("staticstring")]
public ActionResult<string> GetStaticString()
{
var bigString = new String('x', 10 * 1024);
_staticStrings.Add(bigString);
return bigString;
}
前面的代码:
在上图中:
某些方案(如缓存)需要保留对象引用,直到内存压力强制释放它们。 WeakReference类可用于这种类型的缓存代码。 WeakReference对象在内存压力下收集。 的默认实现 IMemoryCache 使用 WeakReference 。
本机内存
某些 .NET Core 对象依赖本机内存。 GC 无法 收集本机内存。 使用本机内存的 .NET 对象必须使用本机代码释放它。
.NET 提供了 IDisposable 接口,使开发人员能够释放本机内存。 即使 Dispose 未调用,也会 Dispose 在 终结器 运行时正确实现类调用。
考虑下列代码:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider 是托管类,因此将在请求结束时收集任何实例。
下图显示了连续调用 API 时的内存配置文件 fileprovider 。
上面的图表显示了此类的实现的一个明显问题,因为它会不断增加内存使用量。 这是 此问题中正在跟踪的已知问题。
可以通过以下方式之一在用户代码中发生相同的泄漏:
大型对象堆
频繁的内存分配/空闲周期可以分段内存,尤其是在分配大块内存时。 对象在连续内存块中分配。 若要缓解碎片,当 GC 释放内存时,它会尝试对其进行碎片整理。 此过程称为 压缩。 压缩涉及移动对象。 移动大型对象会对性能产生负面影响。 出于此原因,GC 将为 大型 对象(称为 大型对象堆 )创建特殊的内存区域, (LOH) 。 大于85000字节的对象 (大约 83 KB) :
当 LOH 已满时,GC 将触发第2代回收。 第2代回收:
以下代码会立即压缩 LOH:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
LargeObjectHeapCompactionMode有关压缩 LOH 的信息,请参阅。
在使用 .NET Core 3.0 和更高版本的容器中,LOH 将自动压缩。
以下 API 演示了此行为:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
下图显示了 /api/loh/84975 在最大负载下调用终结点的内存配置文件:
下图显示调用终结点的内存配置文件 /api/loh/84976 ,只分配 一个字节:
注意:此 byte[] 结构具有开销字节。 这就是84976字节触发85000限制的原因。
比较上述两个图表:
临时大型对象尤其有问题,因为它们会导致 gen2 Gc。
为了获得最佳性能,应最大程度地减少使用的大型对象。 如果可能,请拆分大型对象。 例如,Caching 中的中间件的响应ASP.NET Core 将缓存项拆分为小于85000个字节的块。
以下链接显示了在 LOH 限制下保留对象的 ASP.NET Core 方法:
有关详细信息,请参阅:
HttpClient
使用不当 HttpClient 可能会导致资源泄漏。 系统资源,如数据库连接、套接字、文件句柄等:
经验丰富的 .NET 开发人员知道要对 Dispose 实现的对象调用 IDisposable 。 不释放实现的对象 IDisposable 通常会导致内存泄漏或泄漏系统资源。
HttpClient 实现 IDisposable ,但 不 应在每次调用时都释放。 HttpClient应重复使用。
以下终结点创建并释放 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;
}
}
在 “负载” 下,将记录以下错误消息:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HLG70PBE1CR1", Request id "0HLG70PBE1CR1:00000031":
An unhandled exception was thrown by the application.
System.Net.Http.HttpRequestException: Only one usage of each socket address
(protocol/network address/port) is normally permitted --->
System.Net.Sockets.SocketException: Only one usage of each socket address
(protocol/network address/port) is normally permitted
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port,
CancellationToken cancellationToken)
即使 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 使 实例成为静态实例并由所有请求重复使用。 重复使用可防止资源不足。
对象池:
池是预初始化对象的集合,可以跨线程保留和释放这些对象。 池可以定义分配规则,例如限制、预定义大小或增长率。
此NuGet
Microsoft.Extensions.ObjectPool包包含有助于管理此类池的类。
以下 API 终结点实例化在每个请求 byte 上用随机数字填充的缓冲区:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array); return array;
}
下图显示调用上述具有中等负载的 API:
在上图中,第 0 代回收大约每秒发生一次。
可以通过使用 ArrayPool 对缓冲区进行池化 byte 来优化 <T> 前面的代码。 静态实例在请求之间重复使用。
此方法的不同是,从 API 返回了一个池对象。 也就是说:
设置对象的处置:
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;
}
声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!