从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection

前言

在阅读这篇文章:Announcing Net Core 3 Preview3的时候,我看到了这样一个特性:

Docker and cgroup memory Limits

We concluded that the primary fix is to set a GC heap maximum significantly lower than the overall memory limit as a default behavior. In retrospect, this choice seems like an obvious requirement of our implementation. We also found that Java has taken a similar approach, introduced in Java 9 and updated in Java 10.

大概的意思呢就是在 .NET Core 3.0 版本中,我们已经通过修改 GC 堆内存的最大值,来避免这样一个情况:在 docker 容器中运行的 .NET Core 程序,因为 docker 容器内存限制而被 docker 杀死。

恰好,我在 docker swarm 集群中跑的一个程序,总是被 docker 杀死,大都是因为内存超出了限制。那么升级到 .NET Core 3.0 是不是会起作用呢?这篇文章将浅显的了解 .NET Core 3.0 的 Garbage Collection 机制,以及 Linux 的 Cgroups 内核功能。最后再写一组 实验程序 去真实的了解 .NET Core 3.0 带来的 GC 变化。

GC

CLR

.NET 程序是运行在 CLR : Common Language Runtime 之上。CLR 就像 JAVA 中的 JVM 虚拟机。CLR 包括了 JIT 编译器,GC 垃圾回收器,CIL CLI 语言标准。

那么 .NET Core 呢?它运行在 CoreCLR 上,是属于 .NET Core 的 Runtime。二者大体我觉得应该差不多吧。所以我介绍 CLR 中的一些概念,这样才可以更好的理解 GC

  • 我们的程序都是在操作虚拟内存地址,从来不直接操作内存地址,即使是 Native Code。
  • 一个进程会被分配一个独立的虚拟内存空间,我们定义的和管理的对象都在这些空间之中。
    虚拟内存空间中的内存 有三种状态:空闲 (可以随时分配对象),预定 (被某个进程预定,尚且不能分配对象),提交(从物理内存中分配了地址到该虚拟内存,这个时候才可以分配对象)
  • CLR 初始化GC 后,GC 就在上面说的虚拟内存空间中分配内存,用来让它管理和分配对象,被分配的内存叫做 Managed Heap 管理堆,每个进程都有一个管理堆内存,进程中的线程共享一个管理堆内存
  • CLR 中还有一块堆内存叫做LOH Large Object Heap 。它也是隶属于 GC 管理,但是它很特别,只分配大于 85000byte 的对象,所以叫做大对象,为什么要这么做呢?很显然大对象太难管理了,GC 回收大对象将很耗时,所以没办法,只有给这些 “大象” 另选一出房子,GC 这个“管理员” 很少管 “大象”。

那么什么时候对象会被分配到堆内存中呢?

所有引用类型的对象,以及作为类属性的值类型对象,都会分配在堆中。大于 85000byte 的对象扔到 “大象房” 里。

堆内存中的对象越少,GC 干的事情越少,你的程序就越快,因为 GC 在干事的时候,程序中的其他线程都必须毕恭毕敬的站着不动(挂起),等 GC 说:我已经清理好了。然后大家才开始继续忙碌。所以 GC 一直都是在干帮线程擦屁股的事情。

所以没有 GC 的编程语言更快,但是也更容易产生废物。

GC Generation

那么 GC 在收拾垃圾的过程中到底做了什么呢?首先要了解 CLR 的 GC 有一个Generation 的概念 GC 通过将对象分为三代,优化对象管理。GC 中的代分为三代:

  • Generation 0 零代或者叫做初代,初代中都是一些短命的对象,shorter object,它们通常会被很快清除。当 new 一个新对象的时候,该对象都会分配在 Generation 0 中。只有一段连续的内存
  • Generation 1 一代,一代中的对象也是短命对象,它相当于 shorter object 和 longer object 之间的缓冲区。只有一段连续的内存
  • Generation 2 二代,二代中的对象都是长寿对象,他们都是从零代和一代中选拔而来,一旦进入二代,那就意味着你很安全。之前说的 LOH 就属于二代,static 定义的对象也是直接分配在二代中。包含多段连续的内存

零代和一代 占用的内存因为他们都是短暂对象,所以叫做短暂内存块。 那么他们占用的内存大小是多大?32位和63位的系统是不一样的,不同的GC类型也是不一样的。

WorkStation GC:

32 位操作系统 16MB ,64位 操作系统 256M

Server GC:

32 w位操作系统 65MB,64 位操作系统 4GB!

GC 回收过程

当 管理堆内存中使用到达一定的阈值的时候,这个阈值是GC 决定的,或者系统内存不够用的时候,或者调用 GC.Collect() 的时候,GC 都会立刻可以开始回收,没有商量的余地。于是所有线程都会被挂起(也并不都是这样)

GC 会在 Generation 0 中开始巡查,如果是 死对象,就把他们的内存释放,如果是 活的对象,那么就标记这些对象。接着把这些活的对象升级到下一代:移动到下一代 Generation 1 中。

同理 在 Generation 1 中也是如此,释放死对象,升级活对象。

三个 Generation 中,Generation 0 被 GC 清理的最频繁,Generation 1 其次,Generation 2 被 GC 访问的最少。因为要清理 Generation 2 的消耗太大了。

GC 在每一个 Generation 进行清理都要进行三个步骤:

  • 标记: GC 循环遍历每一个对象,给它们标记是 死对象 还是 活对象
  • 重新分配:重新分配活对象的引用
  • 清理:将死对象释放,将活对象移动到下一代中

WorkStation GC 和 Server GC

GC 有两种形式:WorkStation GCServer GC

默认的.NET 程序都是 WorkStation GC ,那么 WorkStation GC 和 Server GC 有什么区别呢。

上面已经提到一个区别,那就是 Server GC 的 Generation 内存更大,64位操作系统 Generation 0 的大小居然有4G ,这意味着啥?在不调用GC.Collect 的情况下,4G 塞满GC 才会去回收。那样性能可是有很大的提升。但是一旦回收了,4GB 的“垃圾” 也够GC 喝一壶的了。

还有一个很大的区别就是,Server GC 拥有专门用来处理 GC的线程,而WorkStation GC 的处理线程就是你的应用程序线程。WorkStation 形式下,GC 开始,所有应用程序线程挂起,GC选择最后一个应用程序线程用来跑GC,直到GC 完成。所有线程恢复。

而ServerGC 形式下: 有几核 CPU ,那么就有几个专有的线程来处理 GC。每个线程都一个堆进行GC ,不同的堆的对象可以相互引用。

所以在GC 的过程中,Server GC 比 WorkStation GC 更快。但是有专有线程,并不代表可以并行GC 哦。

上面两个区别,决定了 Server GC 用于对付高吞吐量的程序,而WorkStation GC 用于一般的客户端程序足以。

如果你的.NET 程序正在疲于应付 高并发,不妨开启 Server GC : https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcserver-element

Concurrent GC 和 Non-Concurrent GC

GC 有两种模式:ConcurrentNon-Concurrent,也就是并行 GC 和 不并行 GC 。无论是 Server GC 还是 Concurrent GC 都可以开启 Concurrent GC 模式或者关闭 Concurrent GC 模式。

Concurrent GC 当然是为了解决上述 GC 过程中所有线程挂起等待 GC 完成的问题。因为工作线程挂起将会影响 用户交互的流畅性和响应速度。

Concurrent 并行实际上 只发生在Generation 2 中,因为 Generation 0 和 Generation1 的处理是在太快了,相当于工作线程没有阻塞。

在 GC 处理 Generation 2 中的第一步,也就是标记过程中,工作线程是可以同步进行的,工作线程仍然可以在 Generation 0 和 Generation 1 中分配对象。

所以并行 GC 可以减少工作进程因为GC 需要挂起的时间。但是与此同时,在标记的过程中工作进程也可以继续分配对象,所以GC占用的内存可能更多。

而Non-Concurrent GC 就更好理解了。

.NET 默认开启了 Concurrent 模式,可以在 https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcconcurrent-element 进行配置

Background GC

又来了一种新的 GC 模式: Background GC 。那么 Background GC 和 Concurrent GC 的区别是什么呢?在阅读很多资料后,终于搞清楚了,因为英语水平不好。以下内容比较重要。

首先:Background GC 和 Concurrent GC 都是为了减少 因为 GC 而挂起工作线程的时间,从而提升用户交互体验,程序响应速度。

其次:Background GC 和 Concurrent GC 一样,都是使用一个专有的GC 线程,并且都是在 Generation 2 中起作用。

最后:Background GC 是 Concurrent GC 的增强版,在.NET 4.0 之前都是默认使用 Concurrent GC 而 .NET 4.0+ 之后使用Background GC 代替了 Concurrent GC。

那么 Background GC 比 Concurrent GC 多了什么呢:

之前说到 Concurrent GC 在 Generation 2 中进行清理时,工作线程仍然可以在 Generation 0/1 中进行分配对象,但是这是有限制的,当 Generation 0/1 中的内存片段 Segment 用完的时候,就不能再分配了,知道 Concurrent GC 完成。而 Background GC 没有这个限制,为啥呢?因为 Background GC 在 Generation 2 中进行清理时,允许了 Generation 0/1 进行清理,也就说是当 Generation 0/1 的 Segment 用完的时候, GC 可以去清理它们,这个GC 称作 Foreground GC ( 前台GC ) ,Foreground GC 清理完之后,工作线程就可以继续分配对象了。

所以 Background GC 比 Concurrent GC 减少了更多 工作线程暂停的时间。

GC 的简单概念就到这里了以上是阅读大量英文资料的精短总结,如果有写错的地方还请斧正。

作为最后一句总结GC的话:并不是使用了 Background GC 和 Concurrent GC 的程序运行速度就快,它们只是提升了用户交互的速度。因为 专有的GC 线程会对CPU 造成拖累,此外GC 的同时,工作线程分配对象 和正常的时候分配对象 是不一样的,它会对性能造成拖累。

.NET Core 3.0 的变化

  • 堆内存的大小进行了限制:max (20mb , 75% of memory limit on the container)
  • ServerGC 模式下 默认的Segment 最小是16mb, 一个堆 就是 一个segment。这样的好处可以举例来说明,比如32核服务器,运行一个内存限制32 mb的程序,那么在Server GC 模式下,会分配32个Heap,每个Heap 大小是1mb。但是现在,只需要分配2个Heap,每个Heap 大小16mb。
  • 其他的就不太了解了。

实际体验

从开头的 介绍 ASP.NET Core 3.0 文章中了解到 ,在 Docker 中,对容器的资源限制是通过 cgroup 实现的。cgroup 是 Linux 内核特性,它可以限制 进程组的 资源占用。当容器使用的内存超出docker的限制,docker 就会将改容器杀死。在之前 .NET Core 版本中,经常出现 .NET Core 应用程序消耗内存超过了docker 的 内存限制,从而导致被杀死。而在.NET Core 3.0 中这个问题被解决了。

为此我做了一个实验。

这是一段代码:

using System;
using System.Collections.Generic;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
                if (GCSettings.IsServerGC == true)
                    Console.WriteLine("Server GC");
            else
                    Console.WriteLine("GC WorkStationGC");
            byte[] buffer;
            for (int i = 0; i <= 100; i++)
            {
                buffer = new byte[ 1024 * 1024];

                Console.WriteLine($"allocate number {i+1} objet ");

                var num = GC.CollectionCount(0);
                var usedMemory = GC.GetTotalMemory(false) /1024 /1024;

                Console.WriteLine($"heap use {usedMemory} mb");
                Console.WriteLine($"GC occurs {num} times");
                Thread.Sleep(TimeSpan.FromSeconds(5));
           }
        }
    }
}

这段代码是在 for 循环 分配对象。buffer = new byte[1024 * 1024] 占用了 1M 的内存
这段代码分别在 .NET Core 2.2 和 .NET Core 3.0 运行,完全相同的代码。运行的内存限制是 9mb

.NET Core 2.2 运行的结果是:

GC WorkStationGC
allocate number 1 objet
heap use 1 mb
GC occurs 0 times
allocate number 2 objet
heap use 2 mb
GC occurs 0 times
allocate number 3 objet
heap use 3 mb
GC occurs 0 times
allocate number 4 objet
heap use 1 mb
GC occurs 1 times
allocate number 5 objet
heap use 2 mb
GC occurs 1 times
allocate number 6 objet
heap use 3 mb
GC occurs 1 times
allocate number 7 objet
heap use 4 mb
GC occurs 2 times
allocate number 8 objet
heap use 5 mb
GC occurs 3 times
allocate number 9 objet
heap use 6 mb
GC occurs 4 times
allocate number 10 objet
heap use 7 mb
GC occurs 5 times
allocate number 11 objet
heap use 8 mb
GC occurs 6 times
allocate number 12 objet
heap use 9 mb

Exit

首先.NET Core 2.2默认使用 WorkStation GC ,当heap使用内存到达9mb时,程序就被docker 杀死了。

在.NET Core 3.0 中

GC WorkStationGC
allocate number 1 objet
heap use 1 mb
GC occurs 0 times
allocate number 2 objet
heap use 2 mb
GC occurs 0 times
allocate number 3 objet
heap use 3 mb
GC occurs 0 times
allocate number 4 objet
heap use 1 mb
GC occurs 1 times
allocate number 5 objet
heap use 2 mb
GC occurs 1 times
allocate number 6 objet
heap use 3 mb
GC occurs 1 times
allocate number 7 objet
heap use 1 mb
GC occurs 2 times
allocate number 8 objet
heap use 2 mb
GC occurs 2 times
allocate number 9 objet
heap use 3 mb
GC occurs 2 times
....

运行一直正常没问题。

二者的区别就是 .NET Core 2.2 GC 之后,堆内存没有减少。为什么会发生这样的现象呢?

一下是我的推测,没有具体跟踪GC的运行情况
首先定义的占用 1Mb 的对象,由于大于 85kb 都存放在LOH 中,Large Object Heap,前面提到过。 GC 是很少会处理LOH 的对象的, 除非是 GC heap真的不够用了(一个GC heap包括 Large Object Heap 和 Small Object Heap)由于.NET Core 3.0 对GC heap大小做了限制,所以当heap不够用的时候,它会清理LOH,但是.NET Core 2.2 下认为heap还有很多,所以它不清理LOH ,导致程序被docker杀死。

我也试过将分配的对象大小设置小于 85kb, .NET Core 3.0 和.NET Core2.2 在内存限制小于10mb都可以正常运行,这应该是和 GC 在 Generation 0 中的频繁清理的机制有关,因为清理几乎不消耗时间,不像 Generation 2, 所以在没有限制GC heap的情况也可以运行。

我将上述代码 发布到了 StackOverFlow 和Github 进行提问,

https://stackoverflow.com/questions/56578084/why-doesnt-heap-memory-used-go-down-after-a-gc-in-clr

https://github.com/dotnet/coreclr/issues/25148

有兴趣可以探讨一下。

总结

.NET Core 3.0 的改动还是很大滴,以及应该根据自己具体的应用场景去配置GC ,让GC 发挥最好的作用,充分利用Microsoft 给我们的权限。比如启用Server GC 对于高吞吐量的程序有帮助,比如禁用 Concurrent GC 实际上对一个高密度计算的程序是有性能提升的。

参考文章

原文地址:https://www.cnblogs.com/dacc123/p/10980718.html

时间: 2024-10-11 08:48:02

从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection的相关文章

asp.net core 1.1 项目升级至 asp.net core 2.0 preview 2

这两天把一个 asp.net core 1.1 的项目迁移到了 asp.net core 2.0 preview 2 ,在这篇随笔中记录一下. 如果项目在有 global.json 文件,需要删除或修改为 .net 2.0 preview 2 的 sdk 版本号. 对于类库项目的 .csproj,需要把 TagetFramework 改为 netstandard2.0 ,比如 <PropertyGroup> <TargetFramework>netstandard1.6</T

.NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新

原文:.NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新 我们都知道在6月12日的时候微软发布了.NET Core 3.0的第6个预览版.针对.NET Core 3.0的发布我们国内的微软MVP-汪宇杰还发布的官翻版的博文进行了详细的介绍.具体的可以关注"汪宇杰博客"公众号,或者我的"DotNetCore实战"公众号然后在历史文章里面进行查阅.而我们这篇文章将会介绍本次更新中对ASP.NET Core和Blazor所做的更

mazing ASP.NET Core 2.0【转】

前言 ASP.NET Core 的变化和发展速度是飞快的,当你发现你还没有掌握 ASP.NET Core 1.0 的时候, 2.0 已经快要发布了,目前 2.0 处于 Preview 1 版本,意味着功能已经基本确定,还没有学习过 ASP.NET Core 的同学可以直接从 2.0 开始学起,但是如果你已经掌握了 1.0 的话,那么你只需要了解在 2.0 中增加和修改的一些功能即可. 每一次大版本的发布和升级,总会带给开发人员一些惊喜和令人兴奋的特性,有关 ASP.NET Core 本次的 2.

自动挡换手动挡:在 ASP.NET Core 3.0 Middleware 中手动执行 Controller Action

由于遭遇 System.Data.SqlClient 的性能问题(详见之前的博文),向 .NET Core 3.0 的升级工作被迫提前了.在升级过程中遇到了一个问题,我们在 Razor Class Library 中实现的自定义错误页面无法在 ASP.NET Core 3.0 Preview 5 中正常工作,问题原因详见博问 Razor Class Library 中的属性路由在 ASP.NET Core 3.0 中不起作用 . 由于属性路由不起作用的问题没找到解决方法,于是被迫采用了另外一种解

说说ASP.Net Core 2.0中的Razor Page

随着.net core2.0的发布,我们可以创建2.0的web应用了.2.0中新东西的出现,会让我们忘记老的东西,他就是Razor Page.下面的这篇博客将会介绍ASP.Net Core 2.0中的Razor Page. 在ASP.Net Core 2.0新特点之一就是支持Razor Page.今天的Razor Page是ASP.Net Core MVC中的一个子集.ASP.Net Core MVC 支持Razor Page意味着Razor Page应用从技术上来说就是MVC应用,同时Razo

.NET Core 3.0 Preview 5 亮点之一:发布单文件可执行程序

在阅读 Announcing .NET Core 3.0 Preview 5 时发现了 .NET Core 3.0 Preview 5 的一个新特性 —— Publishing Single EXEs ,可以通过 dotnet publish 命令将整个 .net core 应用发布为一个可执行文件.一个期待很久的特性,终于随着 .NET Core 3.0 Preview 5 来了,赶紧体验一下. 用 dotnet new mvc --no-https 创建一个 asp.net core 应用,

【译】宣告推出.NET Core 3.0 Preview 7(英雄的黎明)

今天,我们宣布推出.NET Core 3.0 Preview 7.我们已经从创建新特性阶段过渡到了完善版本阶段.对于接下来的预览版,我们将把重点放在质量(改进)上. 在Windows,macOS和Linux上下载.NET Core 3.0 Preview 7. .NET Core 3.0发行说明 API差异 GitHub发布 报告问题 ASP.NET Core和EF Core今天也在发布更新. Visual Studio用户需要Visual Studio 2019 16.3 Preview 1(

.NET Core 3.0及ASP.NET Core 3.0 前瞻

前几天微软发布了 .NET Core 3.0 Preview 9 ,这是.NET Core 3.0 最后一个预览版. .NET Core 3.0 正式发布将在.NET Conf 上发布,.NET Conf 时间是9月23日至25日. Visual Studio 2019 16.3预览版3和Visual Studio for Mac 8.3支持.NET Core 3.0 ,这些版本也同时发布. 从.NET Core 3.0 Preview 7就可用于生产,目前dotnet官网就是使用 https:

asp.net core 2.0 web api基于JWT自定义策略授权

JWT(json web token)是一种基于json的身份验证机制,流程如下: 通过登录,来获取Token,再在之后每次请求的Header中追加Authorization为Token的凭据,服务端验证通过即可能获取想要访问的资源.关于JWT的技术,可参考网络上文章,这里不作详细说明, 这篇博文,主要说明在asp.net core 2.0中,基于jwt的web api的权限设置,即在asp.net core中怎么用JWT,再次就是不同用户或角色因为权限问题,即使援用Token,也不能访问不该访