图片网站模版,管理咨询师考试,wordpress简单用户积分,机械外协加工网最新订单提起GC大家肯定不陌生#xff0c;但是让大家是说一下GC是怎么运行的#xff0c;可能大多数人都不太清楚#xff0c;这也很正常#xff0c;因为GC这东西在.NET基本不用开发者关注#xff0c;它是依靠程序自动判断来释放托管堆的#xff0c;我们基本不需要主动调用Collect(… 提起GC大家肯定不陌生但是让大家是说一下GC是怎么运行的可能大多数人都不太清楚这也很正常因为GC这东西在.NET基本不用开发者关注它是依靠程序自动判断来释放托管堆的我们基本不需要主动调用Collect()释放内存只需要注意对非托管资源进行及时释放就行。 虽说我们不用关注GC的运行但是作为一个合格的程序员还是有必要知道她是怎么工作的因为垃圾回收对于一个程序来说真的太重要了下面我们就用实际的应用来看下GC在.NET Core下是怎么工作的
GC 会分配堆段其中每个段都是一系列连续的内存。 置于堆中的对象归类为 3 个代系之一0、1 或 2。 代系可确定 GC 尝试在应用不再引用的托管对象上释放内存的频率。 编号较低的代系会更加频繁地进行 GC。
对象会基于其生存期从一个代系移到另一个代系。 随着对象生存期延长它们会移到较高代系。 如前所述较高代系进行 GC 的频率较低。 短期生存的对象始终保留在第 0 代中。 例如在 Web 请求存在期间引用的对象的生存期较短。 应用程序级别单一实例通常会迁移到第 2 代。
当 ASP.NET Core 应用启动时GC 会
为初始堆段保留一些内存。在运行时加载时提交一小部分内存。
进行以上内存分配是出于性能方面的原因。 性能优势来自连续内存中的堆段。
GitHub 上提供了 MemoryLeak 示例应用。 MemoryLeak 应用
运行起来是这样的 Allocated托管对象占用的内存量当前系统认为要分配内存量Working set进程的虚拟地址空间中当前驻留在物理内存中的页集。 显示的工作集与任务管理器显示的值相同。为当前进程分配的物理内存量Gen 0表示第0代堆段被回收Gen 1表示第1代堆段被回收同时回收0、1代Gen 2表示第2代堆段被回收同时回收0、1、2代RPS每秒请求数
GC Server asp.net 默认的是服务端GC
示例提供了很多接口用于调用测试 暂时性对象
我们先来看一下第一个接口
[HttpGet(bigstring)]
public ActionResultstring GetBigString()
{return new String(x, 10 * 1024);
}
创建一个 10-KB 字符串实例并将它返回给客户端。 对于每个请求会在内存中分配一个新对象并将它写入响应中。 字符串作为 UTF-16 字符存储在 .NET 中因此每个字符都需要 2 字节内存。
使用压力测试工具Apache JMeter - Apache JMeter™
对这个接口进行压力测试来观察内存使用情况
运行压力测试工具后可以看到内存使用及GC运行情况
可以看到RPS在3K左右当内存使用量升到了100M左右GC进行了第0代垃圾回收第 0 代 GC 回收大约每两秒进行一次内存消耗和释放通过 GC是稳定的很少出现第1代回收是因为分配的内存都是临时的小内存并发量也在程序的可处理范围内基本在第0代就可以完全回收。 持久性对象引用
接下来看下一个接口 private static ConcurrentBagstring _staticStrings new ConcurrentBagstring();[HttpGet(staticstring)]public ActionResultstring GetStaticString(){var bigString new String(x, 10 * 1024);_staticStrings.Add(bigString);return bigString;} GC 无法释放上面的静态资源对象。 引用了不再需要的对象会导致内存泄露。 如果应用经常分配对象但在不再需要对象之后未能释放它们则内存使用量会随着时间推移而增加。
上面的 API 创建一个 10-KB 字符串实例并将它返回给客户端。 与上一个示例的不同之处在于此实例由静态成员引用这意味着它不能被GC回收。
运行压力测试工具后可以看到内存使用及GC运行情况
在上图中
对/api/staticstring进行压力测试会导致内存线性增加。GC 会在内存压力增加时通过调用Collect来尝试释放内存但是基本无济于事。GC 无法释放泄漏的内存。 已分配内存和工作集会随时间而增加。
停止压力测试后内存还在持续占用手动调用Collect()也无济于事
我们只能调用Clear()来清理静态资源然后Collect()进行回收 [HttpGet(clear)]public ActionResultstring Clear(){_staticStrings.Clear();GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();return ;} 本机内存
来看下一个接口 [HttpGet(fileprovider)]
public void GetFileProvider()
{var fp new PhysicalFileProvider(TempPath);fp.Watch(*.*);
} 某些 .NET Core 对象依赖于本机内存。 GC 无法回收本机内存。 使用本机内存的 .NET 对象必须使用本机代码进行释放。
.NET 提供了 IDisposable 接口使开发人员能够释放本机内存。 即使未调用 Dispose正确实现的类也会在终结器运行时调用 Dispose。
PhysicalFileProvider 是托管类因此任何实例在请求结束时都会被回收。
运行压力测试工具后可以看到内存使用及GC运行情况
上面的图表显示此类的实现存在一个明显问题它会不断增加内存使用量这是因为忘记调用应释放的相关对象的 Dispose 方法
我改一下这个接口使用using调用Dispose private static readonly string TempPath Path.GetTempPath();[HttpGet(fileprovider)]public void GetFileProvider(){using (var fp new PhysicalFileProvider(TempPath)){fp.Watch(*.*);} }
可以看到内存得到了稳定的释放GC调用也相对稳定 大型对象堆
频繁的内存分配/释放周期可能会导致内存碎片尤其是在分配大型内存区块时。 对象在连续内存块中进行分配。 为了减少碎片当 GC 释放内存时它会尝试对其进行碎片整理。 此过程称为压缩。 压缩涉及移动对象。 移动大型对象会造成性能损失。 因此GC 会为大型对象创建特殊内存区域称为大型对象堆 (LOH)。 大于 85,000 字节大约 83 KB的对象
置于 LOH 上。不进行压缩。在第 2 代 GC 期间进行回收。
当 LOH 已满时GC 会触发第 2 代回收。 第 2 代回收
在本质上速度较慢。还会产生对所有其他代系触发回收的成本。
下面的代码会立即压缩 LOH GCSettings.LargeObjectHeapCompactionMode GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); 在使用 .NET Core 3.0 及更高版本的容器中LOH 会自动压缩。
我们来看下一个api
[HttpGet(loh/{size85000})]
public int GetLOH1(int size)
{return new byte[size].Length;
}
用压力测试工具调用 /api/loh/84975 这个接口 换一下对象大小 调用 /api/loh/84976 这个接口 比较上面两个图表
工作集对于这两种方案是相似的大约 450 MB。低于 LOH 请求84,975 字节大部分显示第 0 代回收。高于 LOH 请求84,976 字节生成恒定的第 2 代回收。 第 2 代回收成本高昂。 需要更多 CPU 84,976 字节会就触发了 85,000 限制
所以临时大型对象有性能问题因为它们会导致第 2 代 GC。
为了获得最佳性能应最大程度减少大型对象使用。 如果可能请拆分大型对象。 例如ASP.NET Core 中的响应缓存中间件会将缓存项拆分为小于 85,000 字节的块。