从CLR GC到CoreCLR GC看.NET Core对云原生的支持

内存分配概要

前段时间在园子里看到有人提到了GC学习的重要性,很赞同他的观点。充分了解GC能够帮助咱们更好的认识.NET的设计以及为什么在云原生开发中.NET Core会占有更大的优点,这也是一个程序员成长到更高层次所须要经历的过程。在认识GC的过程当中,咱们先看一下.NET中内存分配的概要知识。
.NET分配内存,主要依据托管资源和非托管资源进行分配。托管资源分配到了托管堆中并受CLR的管理,非托管资源分配到了非托管堆中。该节主要讨论托管资源的分配。
CLR支持两种基本类型:值类型和引用类型。CLR对这两种类型在运行时有两种分配方式:html

CLR内存操做

内存的分配过程以下图所示git

memeryallot

须要注意的是,CLR还要维护一个指针,称为NextObjPtr,这个指针指向下一个对象再堆中的分配位置。初始化时,NextObjPtr设为地址空间区域的基地址。一个区域被非垃圾对象填满后,CLR会分配更多的区域,指针也会不断偏移。new操做符会返回对象的引用,就在返回这个引用以前,NextObjPtr指针的值会加上对象占用的字节数来获得一个新值,即下一个对象放入托管堆时的地址。程序员

nextobjectptr

垃圾回收算法与GC运行机制

经常使用的垃圾回收算法主要有引用计数算法和引用跟踪算法。引用计算有着明显的缺陷,.NET使用的垃圾回收算法是引用跟踪法。小记:关于垃圾回收算法,我记得有一个知识点,在C#中若是出现了循环引用是否会致使内存溢出?若是比较了解这两种算法就会知道不会溢出。github

GC Root

引用跟踪算法,经过一系列GCRoot对象做为起始点,从这些点开始向下搜索,搜索的路径成为引用链,当一个对象到GC没有任何引用链,说明对象能够被回收。算法

GC Root能够类比树来解释docker

gcroot

GC根节点存在于堆栈中,指向Teacher引用对象。它包含一个ArrayList订单集合,由Teacher对象引用。集合自己也包含对其元素的引用,随着搜索深度的增长,树也不断长大。缓存

GC根节点的引用源来自安全

(1)、堆栈
(2)、全局或静态变量
(3)、CPU寄存器
(4)、互操做引用(COM / API调用中使用的.NET对象)
(5)、对象终结引用(objects finalization references)服务器

GC运行机制

GC引入了代的概念,分为三种代,G0、G一、G2,G0对象生存周期较短,越日后生存周期越长(虽然G2中因为直接存储了大对象,又因为G2不是每次都会扫描,因此大多数状况下,G2中的对象的生存周期比G0中的更长)。多线程

GC运行以下图所示

GC flow

须要注意的是,CLR想要进行垃圾回收时,会当即挂起执行托管代码中的全部线程,正在执行非托管代码的线程不会挂起。因此再多线程环境下,可能会出现莫名其妙的诡异问题。

下图为GC的总体运行流程,包含五个步骤:

gcflow

垃圾回收时机与模式

CLR会在一下状况发生时,执行GC操做

      一、当GC的代的预算大小已经达到阈值而没法对新对象分配空间的时候,好比GC的第0代已满;

  二、显式调用System.GC.Collect()(显示调用要慎重,由于手动调用可能会与自动执行的GC冲突,从而致使没法预知的问题);

  三、其余特殊状况,好比,操做系统内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端状况下系统参数设置改变也可能致使GC回收。

关于GC模式主要有

  • WorkStation GC
  • Server GC
  • Concurrent GC
  • Non-Concurrent GC
  • Background GC

详细信息请参阅:http://www.javashuo.com/article/p-untiwtry-ea.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繁忙时间
  • 尝试分配更少的线程,由于添加更多的线程不会提升吞吐量

参考资料:

https://devblogs.microsoft.com/dotnet/using-net-and-docker-together-dockercon-2019-update/

https://github.com/dotnet/designs/blob/master/accepted/support-for-memory-limits.md

http://www.javashuo.com/article/p-untiwtry-ea.html

https://blog.csdn.net/koudaidai/article/details/7794793

相关文章
相关标签/搜索