浅入 .NET Core 中的内存和GC知识
参考资料:
【1】https://docs.microsoft.com/zh-cn/dotnet/standard/managed-code
【2】:https://docs.microsoft.com/zh-cn/dotnet/standard/clr
托管代码
在 .NET 中, CLR(Common Language Runtime) 负责提取托管代码并编译成机器语言,然后执行它。在此过程中,CLR 提供自动内存管理、安全边界、类型安全等服务,保证了代码安全。
托管代码指在其执行过程中由 CLR(Common Language Runtime) 管理的代码,托管代码是可在 .NET 上运行得一种高级语言(C#、F#等),编写的托管代码被编译后会被生成 中间语言(IL)。
CLR 有 .NET Core/.NET5+、Mono、.NET Framework 等实现,托管代码生成的文件(IL代码)不能被操作系统直接运行,需要 CLR 的实现(如 .NET5) 托管运行,托管过程中对其再次编译生成二进制代码(JIT编译)。
中间语言(IL)有时也称为公共中间语言 (CIL) 或 Microsoft 中间语言 (MSIL)。
自动内存管理
自动内存管理是 CLR 的功能之一,它可以为应用程序管理内存的分配和释放,托管代码被执行时,由 CLR 进行内存管理,保证了内存安全。
垃圾回收
GC
GC(garbage collector)中文译为垃圾回收器,.NET 中的 GC 指的是 CLR 中的自动内存管理器,GC 负责管理 .NET 程序的内存分配和释放。
GC 的优点如下:
自动管理内存,不必手动分配和释放;
高效管理托管堆上的对象;
智能回收对象,清除内存;
内存安全:避免野指针、悬空指针等情况造成严重错误;
内存
物理内存
物理内存是物理内存条上的内存空间,是物理机器真实的容量大小。
虚拟内存
虚拟内存(Virtual Memory)是计算机操作系统进行内存管理的一种技术,它可以将多个硬件、非连续地址的碎片空间组合起来,形成进程上可识别的连续内存空间。
虚拟内存由操作系统进行支持,如 Windows 上的虚拟内存,Linux 上的交互空间,虚拟内存需要操作系统映射到真实的内存地址空间才能使用。虚拟内存调度方式有分页式、段式、段页式3种,读者感兴趣可自行查阅资料。
现代操作系统都采用了虚拟内存管理技术,通过对物理存储设备的抽象,操作系统调度外存当作内存使用,提供了比物理内存更大的内存范围。
这些存储设备组成的内存称为虚拟地址空间,而用户(开发者)接触到的地址是虚地址,并不是真实的物理地址。虚拟空间大大拓展了内存,使得系统可以同时运行多道程序而不“吃力”。
虚拟地址空间分为两部分:用户空间、内核空间,每个程序运行时的会消耗两种空间。在 Linux 中比例是 3:1,在 Windows 中是 2:2。
.NET 内存组成
.NET 中,内存分为非托管内存、托管内存。
.NET Core/.NET5+ 有一个称为 dotnet 的驱动程序,此驱动程序用于执行命令或运行 .NET 程序。当我们使用 dotnet 命令运行一个 .dll 文件时,操作系统会启动 dotnet 驱动程序,此时会分配操作系统内存资源、dotnet 驱动程序内存资源,这一部分即非托管资源,其中 dotnet 部分的内存包含了 CLR 等部件的内存。即使你并没有使用到 C/C++ 等非托管代码或者使用非托管资源,也会使用到非托管内存。
接下来 CLR 将初始化新进程,CLR 将为其分配托管内存(托管堆),这段托管内存是一个连续的地址空间区域。.NET 安全代码只能使用托管内存,不能直接使用物理内存,垃圾收集器会为安全代码在托管堆上分配和释放虚拟内存。
显然, dotnet 的工作原理十分复杂,笔者没有能力讲清楚,感兴趣的读者可以自行查阅资料。
CLR 中的内存
微软 .NET CLR 文档中写道:By default, on 32-bit computers, each process has a 2-GB user-mode virtual address space.
即在 32 位系统中,.NET 进程会使用 2GB 的用户模式虚拟内存,其虚拟地址空间的表示范围是 0x00000000 到 0x7fff;而 64 位系统中,地址范围是 0x000'00000000 到0x7FFF'FFFFFFFF,约等于 16TB。
从以上信息,我们知道 .NET 程序会消耗比较多的虚拟内存,如果在 64 位操作系统上运行 .NET 程序,其用户模式虚拟地址空间可能远远大于 2GB。
编写一个 "c1" 程序,其代码如下:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Console.Read();
}
在 Linux 中使用 dotnet xx.dll 命令运行程序,然后查看其占用的资源:
VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3.1g 0.0g 0.0g S 0.3 0.3 0:00.83 dotnet
使用 dotnet-counters 查看 dotnet 进程:
GC Heap Size (MB) 0
Gen 0 GC Count (Count / 1 sec) 0
Gen 0 Size (B) 0
Gen 1 GC Count (Count / 1 sec) 0
Gen 1 Size (B) 0
Gen 2 GC Count (Count / 1 sec) 0
Gen 2 Size (B) 0
LOH Size (B) 0
注:使用 dotnet run 运行 .NET 项目,会出现 dotnet、c1 两个进程,可以看到会产生 dotnet 和 c1 两个进程,dotnet 是驱动程序,dotnet 启动后,CLR 会将. dll 程序集编译,并初始化启动一个进程。
CLR 中的虚拟地址空间需要位于一个地址块中,因为在请求虚拟内存分配时,虚拟内存管理器必须找到满足需求的单个可用块,例如就算存在大于 2GB 的虚拟地址空间,但如果不是连续的,则会分配失败。如果没有足够的可供保留的虚拟地址空间或可供提交的物理空间,则可能会用尽内存。
CLR 虚拟内存状态
CLR 中的虚拟内存可以有三种状态:
State | Description |
---|---|
Free 可用 | The block of memory has no references to it and is available for allocation. 内存块没有对它的引用,可以进行分配 |
Reserved保留 | The block of memory is available for your use and cannot be used for any other allocation request. 该内存块可供您使用,不能用于任何其他分配请求 However, you cannot store data to this memory block until it is committed. 但是,在提交数据之前,不能将数据存储到此内存块中 |
Committed已提交 | The block of memory is assigned to physical storage. 内存块已指派给物理存储 |
内存分配
CLR 在初始化新进程时,会为进程保留一个连续的地址空间区域,这个地址空间被称为托管堆。托管堆中维护着一个指针,最初此指针指向托管堆的基址,这个指针是向后移动的。当需要分配内存时,CLR 便会分配位于此指针后的内存区域,同时指针指向此对象地址空间之后的位置。
由于 CLR 通过向指针添加值来为对象分配内存,所以它的分配速度几乎跟从堆栈中分配内存速度一样快;而且连续分配的新对象连续存储在托管堆中,程序可以快速地访问这些对象。
当 GC 回收内存时,一些对象释放后内存会被回收,这样托管堆地内存处于碎片化,之后整个内存段会被压缩,重新组成连连续的内存段,指针会被重置到对象的末尾。
当然,大对象堆(LOH)回收并不会压缩内存段,这一点我们后面再讨论。
内存释放
垃圾回收的条件
根据微软官方文档,整理的垃圾回收条件如下:
- 系统物理内存不足;
- 托管堆分配的内存已超出可接受阈值;(当然,这个阈值会被动态调整)
- 手动调用 GC 类的 API(例如 GC.Collect);
托管堆
本机堆(Native Heap)
前面提到过,.NET 的内存有非托管内存和托管内存。CLR 运行的进程,存在本机堆和托管堆两种内存堆,本机内存堆通过 Windows API 的 VirtualAlloc 函数分配,提供给 操作系统和 CLR 使用,用于非托管代码所需的内存。
托管堆(Managed Heap)
关于托管堆,前面已经写了,这里不再赘述。
托管堆代数
托管堆中的内存被分为三代,分别使用0、1、2 标识,GC 分配的内存首先在 0 代托管堆中,当进行垃圾回收时,如果对象没有被释放,则将其升级并存储到 1 代托管堆中。1 代托管堆进行内存回收时,不被释放的对象也会被升级到 2 代内存中,然后 1 代内存堆进行空间压缩。
托管堆的管理是 GC 负责的,而 GC 进行内存分配和释放,使用了 GC 算法。
GC 算法基于以下理论:
- ① 压缩托管堆的一部分内存要比压缩整个托管堆速度快;
- ② 较新的对象生命周期较短,较旧的对象生命周期较长;
- ③ 较新的对象趋向于相互关联,并且大约在同一时间被应用程序访问;
我们必须深刻理解这些理论,才能深入理解托管堆的设计。
关于 0 到 2 代堆,其基本说明如下:
- 0 代:0 代中的对象拥有短暂的生命周期,垃圾回收最常发生在此代中;
- 1 代:作为生命周期较短和生命周期较长对象的缓冲区。
- 2 代:存储生命周期长的对象;0、1 代没被回收而升级的对象会升级到 2 代中,静态数据等则会一开始就分配到 2代。
在 .NET 5 之前,.NET 有 SOH(小对象堆)、LOH(大对象堆);在 .NET 5 中,出现了 POH ;
小对象堆的内存段有 0、1、2 代堆;
今天就水到这里为止。
浅入 .NET Core 中的内存和GC知识的更多相关文章
- Window中的内存地址(小知识)
现在的编辑器大部分工作都是内存管理托管型,所以很少直接对Window的内存地址直接管理了. Window中的内存地址主要是以16进制数字体现的,当操作系统为32位时,那么每个内存地址为2的32次方,也 ...
- 浅谈.Net Core中使用Autofac替换自带的DI容器
为什么叫 浅谈 呢?就是字面上的意思,讲得比较浅,又不是不能用(这样是不对的)!!! Aufofac大家都不陌生了,说是.Net生态下最优秀的IOC框架那是一点都过分.用的人多了,使用教程也十分丰富, ...
- 『浅入深出』MySQL 中事务的实现
在关系型数据库中,事务的重要性不言而喻,只要对数据库稍有了解的人都知道事务具有 ACID 四个基本属性,而我们不知道的可能就是数据库是如何实现这四个属性的:在这篇文章中,我们将对事务的实现进行分析,尝 ...
- 浅入Kubernetes(6):CKAD认证中的部署教程
目录 预设网络 kubeadm 安装 k8s 配置 calico 自动补全工具 状态描述 目前为止,笔者已经写了 5 篇关于 k8s 的文章,这一篇笔者将介绍 CKAD 认证官方课程中,如何部署 k8 ...
- 浅入深出之Java集合框架(中)
Java中的集合框架(中) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到<浅入深出之Java集合框架 ...
- .net core中的高效动态内存管理方案
.net core在新增的System.Buffers中引入了一大堆高效内存管理的类,如span和memory.内存池.本文今天这里介绍一个高效动态内存访问方案. ReadOnlySequenceSe ...
- 浅谈ASP.NET Core中IOC与DI的理解和使用
说起IOC和DI,使用过ASP.NET Core的人对这两个概念一定不陌生,早前,自己也有尝试过去了解这两个东西,但是一直觉得有点很难去理解,总觉得对其还是模糊不清,所以,趁着今天有空,就去把两个概念 ...
- 浅谈ASP.NET Core中的DI
DI的一些事 传送门马丁大叔的文章 什么是依赖注入(DI: Dependency Injection)? 依赖注入(DI)是一种面向对象的软件设计模式,主要是帮助开发人员开发出松耦合的应用程序 ...
- Linux就这个范儿 第15章 七种武器 linux 同步IO: sync、fsync与fdatasync Linux中的内存大页面huge page/large page David Cutler Linux读写内存数据的三种方式
Linux就这个范儿 第15章 七种武器 linux 同步IO: sync.fsync与fdatasync Linux中的内存大页面huge page/large page David Cut ...
随机推荐
- 堆叠注入tips
漏洞成因 使用mysqli_multi_query()这种支持多语句执行的函数 使用PDO的方式进行数据查询,创建PDO实例时PDO::MYSQL_ATTR_MULTI_STATEMENTS设置为tr ...
- 模拟数组 push() 方法
var array =[]; Array.prototype.push = function (){ for (var i=0; i< arguments.length; i++){ this[ ...
- 推荐系统实践 0x0d GBDT+LR
前一篇文章我们介绍了LR->FM->FFM的整个演化过程,我们也知道,效果最好的FFM,它的计算复杂度已经达到了令人发指的\(n^2k\).其实就是这样,希望提高特征交叉的维度来弥补稀疏特 ...
- 【题解】Generator(UVA1358)
感觉我字符串和期望都不好-- 题目链接 题意 有 \(n\) 种字符,给定一个模式串 \(S\) ,一开始字符串为空,现在每次随机生成一个 1~n 的字符添加到字符串末尾,直到出现 \(S\) 停止, ...
- MySQL技术内幕InnoDB存储引擎(六)——锁
什么是数据库的锁? 锁是数据库系统区别于文件系统的一个关键特性.锁机制用于管理对共享资源的并发访问.让数据库事务满足隔离性的要求. InnoDB 中锁的作用 不仅用于对数据进行并发访问,还还包括了缓冲 ...
- Editor.md解决跨域上传的问题
Editor.md解决跨域上传的问题 编辑 editormd\plugins\image-dialog\image-dialog.js 替换以下代码片段 if (settings.crossDomai ...
- 如何写好PPT,什么样的PPT容易被人理解记住
PPT一般是用于讲解性的行为而存在,那如果写好PPT呢?如果写好,这个完全要取决于你所面向的目标读者,是用于学术行为呢?还是用于商业行为.面对不同的目标群体,有不同的策略.但是无论面向群体是谁我们都有 ...
- 06 python开发之函数
06 python开发之函数 目录 06 python开发之函数 6 函数 6.1 基本使用 6.1.1 基本概念 6.1.2 定义函数 6.2 调用函数与函数返回值 6.2.1 调用函数三种形式 6 ...
- Jmeter对数据库做压力测试
一.环境:apache-jmeter-5.0,Oracle11g.windows7.jdk1.8.ojdbc14-10.2.0.2.0.jar二.操作配置:2.1.启动Jmeter Jmeter初始化 ...
- Spring MVC例子
学习Spring MVC的一个例子,参考书籍<Servlet.JSP.Spring MVC学习指南>,简单总结一下. 代码下载:https://github.com/PeiranZhang ...