从CLR GC到CoreCLR GC看.NET Core对云原生的支持
文章目录内存分配概要垃圾回收算法与GC运行机制GC RootGC运行机制垃圾回收时机与模式.NET Core 3.0 中的 GC 优化处理GC堆限制支持 Docker 内存限制支持 DockerCPU 限制参考文献内存分配概要前段时间在园子里看到有人提到了 GC 学习的重要性,很赞同他的观点。充分了解 GC 可以帮助我们更好的认识 .NET 的设计以及为何在云原生开发中 .NET Core 会..
文章目录
内存分配概要
前段时间在园子里看到有人提到了 GC 学习的重要性,很赞同他的观点。充分了解 GC 可以帮助我们更好的认识 .NET 的设计以及为何在云原生开发中 .NET Core 会占有更大的优势,这也是一个程序员成长到更高层次所需要经历的过程。在认识 GC 的过程中,我们先看一下.NET中内存分配的概要知识。
.NET 分配内存,主要依据托管资源和非托管资源进行分配。托管资源分配到了托管堆中并受 CLR 的管理,非托管资源分配到了非托管堆中。该节主要讨论托管资源的分配。
CLR 支持两种基本类型:值类型和引用类型。CLR 对这两种类型在运行时有两种分配方式:
内存的分配过程如下图所示:
需要注意的是,CLR 还要维护一个指针,称为 NextObjPtr,这个指针指向下一个对象再堆中的分配位置。初始化时,NextObjPtr 设为地址空间区域的基地址。一个区域被非垃圾对象填满后,CLR 会分配更多的区域,指针也会不断偏移。new 操作符会返回对象的引用,就在返回这个引用之前,NextObjPtr 指针的值会加上对象占用的字节数来得到一个新值,即下一个对象放入托管堆时的地址。
垃圾回收算法与GC运行机制
常用的垃圾回收算法主要有 引用计数算法 和 引用跟踪算法。引用计数有着明显的缺陷,.NET 使用的垃圾回收算法是引用跟踪法。小记:关于垃圾回收算法,我记得有一个知识点,在C#中如果出现了循环引用是否会导致内存溢出?如果比较了解这两种算法就会知道不会溢出。
GC Root
引用跟踪算法,通过一系列 GCRoot 对象作为起始点,从这些点开始向下搜索,搜索的路径成为引用链,当一个对象到GC没有任何引用链,说明对象可以被回收。GC Root可以类比树来解释:
GC 根节点存在于堆栈中,指向Teacher引用对象。它包含一个ArrayList订单集合,由Teacher对象引用。集合本身也包含对其元素的引用,随着搜索深度的增加,树也不断长大。GC根节点的引用源来自:
- (1) 堆栈
- (2) 全局或静态变量
- (3) CPU寄存器
- (4) 互操作引用(COM / API调用中使用的.NET对象)
- (5) 对象终结引用(objects finalization references)
补充GC引用跟踪原理:引用跟踪算法只关心 引用类型 的 变量,因为只有它才能引用堆上的对象。引用类型的变量可以出现在字段、方法参数、局部变量 等场合中。这些所有引用类型的 变量 都称之为 “根(root)”。GC 从遇到的第一个根开始,将根引用的对象进行标记,同时也标记这个被引用的对象中包含的根所引用的对象,如此遍历完所有可达的根之后,就找出了所有正在使用中的对象。不可达的对象就可以销毁了。
GC运行机制
GC引入了代的概念,分为三种代(即,GC将托管堆分成了三个区域):G0、G1、G2。
一个新产生的对象会被首先放在G0中(大对象除外),若G0空间被占满,则触发一次 GC,从而清理G0区域,并把存活的对象移动到G1中,同样地,G1满之后将被GC清理并把存活的对象移动到G2中。
G0对象生存周期较短,越往后生存周期越长(虽然G2中由于直接存储了大对象,又由于G2不是每次都会扫描,所以大多数情况下,G2中的对象的生存周期比G0中的更长)。GC运行机制如下图所示:

需要注意的是,CLR 想要进行垃圾回收时,会立即挂起执行托管代码中的所有线程,正在执行非托管代码的线程不会挂起。所以再多线程环境下,可能会出现莫名其妙的诡异问题。
下图为 GC 的整体运行流程,包含五个步骤:

垃圾回收时机与模式
CLR 会在一下情况发生时,执行 GC 操作:
(1)当 GC 的代的预算大小已经达到阈值而无法对新对象分配空间的时候,比如 GC 的第0代已满;
(2)显式调用System.GC.Collect()(显示调用要慎重,因为手动调用可能会与自动执行的GC冲突,从而导致无法预知的问题);
(3)其他特殊情况,比如,操作系统内存不足、CLR 卸载 AppDomain、CLR 关闭,甚至某些极端情况下系统参数设置改变也可能导致GC回收。
关于GC模式主要有:
- WorkStation GC
- Server GC
- Concurrent GC
- Non-Concurrent GC
- Background GC
详细信息请参阅:https://www.cnblogs.com/dacc123/p/10980718.html,这篇文章关于GC模式的说明比较详细。
.NET Core 3.0 中的 GC 优化处理
.NET Core 3.0 默认更好的支持 Docker 资源限制,官方团队也在努力让 .NET Core 成为真正的容器运行时,使其在低内存环境中具有容器感知功能并高效运行。
GC堆限制
.NET Core 减少了 CoreCLR 默认使用的内存,如 G0 代内存分配预算,以更好地与现代处理器缓存大小和缓存层次结构保持一致。
在新的创建的 GC 堆数量的策略里,GC保留了一个内存片段,每个堆最小是16M,在低内存限制的机器上也可以很好的运行。在多核CPU的机器上运行时,系统并没有设置CPU的核数限制。例如,如果在48核计算机上设置160 MB内存限制,则不需要创建48个GC堆。也就是说如果设置160 MB限制,则只会创建10个GC堆。如果未设置CPU限制,应用程序可以利用计算机上的所有核心。
有了这样的新策略,可以不需要启用 Docker 环境下的 .NET Core 应用的工作站 GC 的工作负载。
支持 Docker 内存限制
Docker 资源限制建立在 cgroup 之上,而 cgroup 是 Linux 的内核功能。从运行时的角度来看,我们需要定位 cgroup 原语。
设置cgroup限制时的 .NET Core 3.0 内存使用规则:
- 默认GC堆大小:容器上
cgroup内存限制的最大值 20MB 或最大值的 75% - 每个GC堆的最小保留段大小16MB,这将减少在具有大量内核和小内存限制的计算机上创建的堆数
为了支持容器方案,添加了2个 HardLimit 配置:
GCHeapHardLimit- 指定GC堆的硬限制GCHeapHardLimitPercent- 指定允许此进程使用的物理内存的百分比
如果同时指定了两者,则首先检查 GCHeapHardLimit,并且只有在未指定 GCHeapHardLimit时才检查GCHeapHardLimitPercent。
如果两者都未指定,但进程正在有内存限制的容器中运行,则默认是使用如下设置:
- max(20mb,容器内存限制的75%)
如果指定了hardlimit配置,并且程序在有内存限制的容器中使用,GC 堆的使用不会超过hardlimit限制,但总内存仍然受容器的内存限制。所以当我们统计内存消耗时,基于容器内存限制得出的数据。
举例:
进程在设置了200MB限制的容器中运行,用户还将GCHeapHardLimit配置为100MB。
如果把GC限制中100MB限制中的50MB用于GC,而容器限制中剩余的100MB用于其他用途,那么内存消耗即为(50+100)/200=75%。
GC 将更积极地执行资源回收与释放,因为 GC 堆越接近 GCHeapHardLimit 限制,就越能实现提供更多可用内存的目标,也越能使得应用程序可以继续而又安全地运行。如果算法计算出的结果认为此时的 GC 效率低下,那么将避免持续执行完全阻塞的 GC。
即使GC堆完全压缩,GC 依然会抛出一个 OutOfMemoryException 异常出来,这是因为所分配的堆大小超过了 GCHeapHardLimit 的限制。
由此可见,.NET Core 3.0 的设计是要稳定运行于有资源限制的容器中。
支持 DockerCPU 限制
在CPU限制的情况下,Docker上设置的值将向上舍入为下一个整数值。此值是 CoreCLR 使用的最大有效CPU核数。
默认情况下,ASP.NET Core 应用程序启用了服务器 GC(它不适用于控制台应用程序),因为它可以实现高吞吐量并减少跨核心的争用。当进程仅限于单个处理器时,运行时会自动切换到工作站GC。即使您明确指定使用服务器GC,工作站GC也将始终用于单核环境。
通过计算CPU繁忙时间,设置CPU限制,我们避免了线程池的各种推导性竞争:
- 尝试分配更多的线程以增加CPU繁忙时间
- 尝试分配更少的线程,因为添加更多的线程不会提高吞吐量
参考文献
[1] Using .NET and Docker Together – DockerCon 2019 Update
[2] Proposal for .NET Core GC Support for Docker Limits
[3] 从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection
[4] .NET下的内存分配机制
更多推荐


所有评论(0)