托管代码
在 .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 中的虚拟内存可以有三种状态:
内存分配
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 代堆;
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持自由互联。