# C# 托管代码和垃圾回收

# 什么是托管代码
托管代码就是执行过程交由运行时管理的代码。运行时一般是指 CLR，公共语言运行时。

> CLR 负责提取托管代码、将其编译成机器代码，然后执行它。除此之外，运行时还负责自动内存管理、安全边界、类型安全等等。

如果在 .Net 里面直接调用 C/C++ 程序，此类代码也称为“非托管代码”。在非托管代码的环境中，操作系统将程序加载进内存，然后调用内部的二进制代码，所以从内存管理到安全等诸多因素都需要程序员自己处理。

正常使用 .Net 编写的代码，会先编译成中间语言 （IL）。执行的时候，CLR 会接管编译后的 IL，然后通过实时编译（JIT）将 IL 编译成可以在CPU上执行的机器码。这样 CLR 就能知道代码的确切作用，并管理代码。

# 分配内存
初始化新进程时，运行时会为进程创建托管堆。

托管堆是一个连续的地址空间区域，托管堆内维护着一个指针，用它指向将在堆中分配的下一个对象的地址。最初的时候，该指针指向托管堆的基址。

当应用程序创建第一个引用类型时，将为托管堆的基址中的类型分配内存。 应用程序创建下一个对象时，垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。 只要地址空间可用，垃圾回收器就会继续以这种方式为新对象分配空间。

严格来说，必须在托管堆上分配所有非 null 引用类型对象和所有装箱值类型对象。

从托管堆中分配内存要比非托管内存分配速度快。 由于运行时通过为指针添加值来为对象分配内存，所以这几乎和从堆栈中分配内存一样快。 另外，由于连续分配的新对象在托管堆中是连续存储，所以应用程序可以快速访问这些对象。

# 释放内存
垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间。

每个应用程序都有一组根。每个根或者指向托管堆中的对象，或者为空。
应用程序的根包含线程堆栈上的静态字段、局部变量和参数以及 CPU 寄存器。

垃圾回收器可以访问由[实时 (JIT) 编译器](https://docs.microsoft.com/zh-cn/dotnet/standard/managed-execution-process)和运行时维护的活动根的列表。垃圾回收器对照此根列表检查应用程序的根，并在此过程中创建一个图表，在其中包含所有可从这些根中访问的对象。

如果有一个对象，不在上面创建的图表中，那意味着这个对象将无法从应用程序的根中访问。这个时候，垃圾回收器就可以考虑释放为它们分配的内存。

回收的时候，垃圾回收器会检查托管堆，查找无法访问的对象所占据的地址块。当发现无法访问的对象的时候，GC 会通过内存复制的方法来压缩内存中可以访问的对象，并释放分配给不可访问对象的地址空间块。在压缩可访问的对象之后，GC 会更正原来指向这个对象的指针，使应用程序的根指向新地址中的对象。在 GC 完成之后，会将托管堆指针指向最后一个可访问的对象。

> 只有在发现了大量无法访问的对象的时候，才会压缩内存。

# 托管堆的级别和性能
为了提高 GC 的性能，托管堆被划分为3代：第0代、第1代和第2代。
> 划分不同代的依据：
> 1. 压缩托管堆的一部分内存比压缩整个托管堆速度快
> 2. 较新的对象生存期较短，较旧的对象生存期较长
> 3. 较新的对象趋向于互相关联，并且大致同时由应用程序访问

新创建的对象储存在第0代。在第0代满了的时候，GC 会执行回收，尝试释放第0代中的地址空间。在第0代的回收执行之后，GC 会将第0代的内存对象升级到第1代。

如果第0代托管堆的回收没有回收足够的内存，不能使应用程序成功完成创建新对象的尝试，垃圾回收器就会先执行第1代托管堆的回收，然后再执行第2代托管堆的回收。如果这样仍不能回收足够的内存，垃圾回收器将执行第2、1 和 0 代托管堆的回收。

 每次回收后，垃圾回收器都会压缩第0代托管堆中的可访问对象并将它们升级至第1代托管堆。 第1代托管堆中未被回收的对象将会升级至第2代托管堆。 由于垃圾回收器只支持三个级别，因此第2代托管堆中未被回收的对象会继续保留在第2代托管堆中，直到在将来的回收中确定它们为无法访问为止。

如果新创建的对象是大型对象，它们将延续到大型对象堆 (LOH)，这有时称为第 3 代。 第 3 代是在第 2 代中逻辑收集的物理生成。

# 非托管资源的内存释放
非托管资源需要显式清除。

最常用的非托管资源类型是包装操作系统资源的对象（例如文件句柄、窗口句柄或网络连接）。GC 可以跟踪封装非托管资源的托管对象的生存期，但却无法具体了解如何清理资源。

创建封装非托管资源的对象时，建议在公共 **Dispose** 方法中提供必要的代码以清理非托管资源。 通过提供 **Dispose** 方法，对象的用户可以在使用完对象后显式释放其内存。 使用封装非托管资源的对象时，应该了解 **Dispose** 并在必要时调用它。

还必须提供一种释放非托管资源的方法，以防类型使用者忘记调用 **Dispose**。 可以使用安全句柄来包装非托管资源，也可以重写 [Object.Finalize()](https://docs.microsoft.com/zh-CN/dotnet/api/system.object.finalize#system-object-finalize) 方法。

# 内存的基本知识
- 每个进程都有其单独的虚拟地址空间
- 同一台计算机上的所有进程共享相同的物理内存和页文件（如果有）
- 默认情况下，32位计算机上的每个进程都具有 2 GB的用户模式虚拟地址空间
- 程序开发人员，默认只能使用虚拟地址空间。GC 会为你分配和释放托管堆上的虚拟内存
- 虚拟内存的三种状态：
	- Free：该内存块没有引用关系，可用于分配
	- Reserved：给你用，其他人不能用。但是在提交之前，无法存储数据
	- Committed：内存块已指派给物理存储
- 如果没有足够的可供保留的虚拟地址空间或可供提交的物理空间，则可能会用尽内存
- 托管堆可以看做大对象推和小对象堆的集合
- 大对象堆包含大小不小于 85,000 个字节的对象，这些对象通常是数组。 非常大的实例对象是很少见的。（可以配置阈值大小）

即使在物理内存压力（即物理内存的需求）较低的情况下也会使用页文件。 首次出现物理内存压力较高的情况时，操作系统必须在物理内存中腾出空间来存储数据，并将物理内存中的部分数据备份到页文件中。 该数据只会在需要时进行分页，所以在物理内存压力较低的情况下也可能会进行分页。

# 垃圾回收的过程
垃圾回收分为以下几个阶段：
-   标记阶段：找到并创建所有活动对象的列表。
-   重定位阶段：用于更新对将要压缩的对象的引用。
-   压缩阶段：用于回收由死对象占用的空间，并压缩幸存的对象。 压缩阶段将垃圾回收中幸存下来的对象移至段中时间较早的一端。

因为第 2 代回收可以占用多个段，所以可以将已提升到第 2 代中的对象移动到时间较早的段中。 可以将第 1 代幸存者和第 2 代幸存者都移动到不同的段，因为它们已被提升到第 2 代。

通常，由于复制大型对象会造成性能代偿，因此不会压缩大型对象堆 (LOH)。 但是，在 .NET Core 和 .NET Framework 4.5.1 及更高版本中，可以根据需要使用 [GCSettings.LargeObjectHeapCompactionMode](https://docs.microsoft.com/zh-CN/dotnet/api/system.runtime.gcsettings.largeobjectheapcompactionmode) 属性按需压缩大型对象堆。 

## 确定活动对象
垃圾回收器使用以下信息来确定对象是否为活动对象：
- **堆栈根**。 由实时 (JIT) 编译器和堆栈查看器提供的堆栈变量。 JIT 优化可以延长或缩短报告给垃圾回收器的堆栈变量内的代码的区域。
- **垃圾回收句柄**。 指向托管对象且可由用户代码或公共语言运行时分配的句柄。
- **静态数据**。 应用程序域中可能引用其他对象的静态对象。 每个应用程序域都会跟踪其静态对象。

## 挂起
在垃圾回收启动之前，除了触发垃圾回收的线程以外的所有托管线程均会挂起。

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1655172950483/wWiwVyrWR.png align="left")


//todo
内存虚拟地址和物理地址的转换过程
