# 前言

本文包含原书第七章至第十五章,也就是本书最后一部分。
主要为对象资源释放、线程局部存储、托管指针、ref 类型、非托管类型、非托管约束、 Span<T>Memory<T> 、Unsafe 类、面向数据设计 等高级主题。

这是本书的最后一份笔记,没想到整体看完这本书花了 2 月有余... 说来惭愧,这两个月花了太多时间在其它东西上去了:玩游戏,比如最近的蔚蓝档案、或者在家看 HDRP 相关的教程。
虽然除了玩游戏也不能说全无相关,但确实是这次没有专心看书了,导致时间线被严重拉长 —— 特别是本来每天就只有下班时间两个小时的情况下。

后面得再紧张一点了。

# 第十二章 对象生存期

# 终结

.NET 有两种终结方式:非确定性终结与确定性终结,也可以称为 隐式清除 和 显式清除

  • 析构函数
  • IDisposable

终结机制和垃圾回收机制在概念上并没有直接关系。
终结器对象特点:

  • 执行时间非确定
  • 执行顺序、及其持有的对象图只有在运行完终结器之后才能被回收
  • 执行线程不确定
  • 不保证终结器代码一定会执行一次 (如出错会阻塞执行线程)
  • 在终结器引发异常会很危险 (无法执行终结器被视为最严重故障)
  • 可终结对象对 GC 有额外开销

因此,推荐显式 IDisposable 释放而非使用终结器
注:值类型可以有终结器,但只会在装箱实例上运行。其在 .NET Core 则直接无效。
调用时间点

  • 当 GC 结束时
  • 当运行时卸载 AppDomain 和运行时终止时

# 关键终结器

CriticalFinalizerObject
关键终结器是一种常规终结器,但其带有额外的保证,保证其代码一定会执行。

  • 是一个不包含实现的抽象类,只是类型系统和运行时之间的一个协议
  • 运行时将采取一些预防措施确保在任何情况下都执行关键终结器 (如运行时会提前 jit 代码,避免后续因 out-of-memory 而无法 jit 编译和执行)

(SafeHandle 继承 CriticalFinalizerObject)

# 终结器内部实现

GC 使用一个 finalization queue (终结器队列) 记录所有可终结对象

  • 在执行 GC 期间,GC 会在标记阶段结束后检查终结器队列,查看是否有可终结对象已死亡
  • 若有,则将其移动到 fReachable queue (可达对象队列),然后通知专门的终结器线程执行
  • 终结器线程执行发生在 GC 将托管线程从挂起恢复到正常状态之后
  • 由于引用这些对象的根已经从终结器队列移除,下一次 GC 时会最终对其执行回收:即可终结对象至少能存活到下一次 GC

注 1:由于终结器线程不一定能在下一次 GC 前执行完毕,fReachable queue 在标记阶段也会被视为一个根,并导致终结器对象被提升到老一代,面临『中年危机』
注 2:.NET 提供一个方法 GC.WaitForPendingFunalizers ,可阻塞线程直到可达对象队列执行完毕
另外还有两个与终结器相关的重要 API:

  • GC.ReRegisterForFinalize(object)
  • GC.SuppressFinalize (object):设置对象标头中的一个 bit,终结器线程工作期间,不会调用此 bit 被设置过的对象的 Finalize 方法

为什么需要专门的 queue 和终结器线程?

  • 终结器代码是用户的,以异步方式执行终结器代码更安全

# 终结器开销

  • 默认情况下,分配对象时强制使用慢速分支
  • 默认情况下,至少会让对象被提升一次,增加对象生存期
  • 如果可终结对象分配速度快于被终结速度,导致危险后果

# 复活

终结器代码可以将自身赋值给某个根,导致此对象重新变为可达,这种情况称为复活。

  • 注:复活后除非重新注册终结器队列,否则死亡时不会再执行终结器代码

使用复活机制的场景之一是对象池的隐式回收,但隐式池化管理并没有特别的好处。

# 终结器注意点

  • 总是检查是否存在期待资源:如从构造函数抛出异常,会导致终结器在对象状态未完全初始化的情况下执行
  • 避免在终结器分配资源:如果出现 OutOfMemory 会是严重错误
  • 避免任何线程上下文依赖
  • 不要在终结器抛出异常
  • 避免从终结器调用 virtual 成员

首选显式清理。

# IDisposable

通常可以将隐式释放的终结器与要求显式释放的 IDisposable 接口结合,若显示调用了 Dispose 方法则使用 GC.SuppressFinalize 抑制终结器的调用,否则以终结器兜底。
注:抑制终结器如上所示,操作很简单,没有性能影响

# 安全句柄

用于安全处理非托管资源的类型,构建于关键终结器 (CriticalFinalizerObject) 之上。

  • 即结合了显式和隐式释放

CLR 在 P/Invoke 期间会以特殊方式处理:不会 (像 HandleRef 那样) 被垃圾回收,而是基于安全原因使用引用计数逻辑

  • 每个 P/Invoke 都包含 JIT 编译的用于递增引用计数逻辑,并在调用结束后递减计数器

相比其它替代者,SafeHandle 好处有:

  • 拥有关键终结器,比常规终结器更可靠
  • 仅对非托管资源 (句柄) 进行了最小化的简单封装
  • 使用 SafeHandle 的对象无需使用终结器
  • 更好的生存期管理:GC 在 P/Invoke 调用期间自动保持其存活状态,无需手动 GC.KeepAlive
  • 有多个对应不同资源的 SafeHandle 子类型,比直接使用 IntPtr 拥有更好的强类型支持
  • 对句柄重用攻击防范使得安全性提高

注:P/Invoke marshaling 机制可以在底层将 SafeHandle 派生类视为 IntPtr (直接将 IntPtr 句柄赋值给 SafeHandle 派生类)

# 预定义安全句柄

  • SafeFileHandle
  • SafeMemoryMappedFileHandle、SafeMemoryMappedViewHandle:内存映射文件句柄相关的安全句柄
  • SafeNCryptKeyHandle、SafeNCrypProviderHandle、SafeNCrypSecretHandle
  • SafePipeHandle:命名管道句柄的安全句柄
  • SafeProcessHandle:用于进程的安全句柄
  • SafeRegistryHandle:用于注册表键的安全句柄
  • SafeWaitHandle:安全等待句柄 (用于同步场景)

注:若底层非托管代码某些部分确实需要直接使用 IntPtr,可以通过 DangerousGetHandle 获取原始句柄。

# 弱引用

存储一个对象的引用,但其自身不被视为一个根 (它对目标对象的引用不会使得后者保持可达状态)

  • 短弱句柄 (short weak handle):当 GC 决定回收对象,在终结器运行之前被清零。即使终结器复活了对象,引用它的弱句柄依然保持清零状态。
  • 长弱句柄 (long weak handle):当对象由于终结被提升时,它们的目标仍然保持有效。如果终结器复活了对象,长弱句柄将保持有效状态。

主要应用场景:

  • 各种类型的观察和侦听器
  • 缓存

.NET 提供:GCHandle 及封装其的 WeakReference 和 WeakReference(请使用 TryGetTarget)
注:WPF 中存在 WeakEventManager 实现了弱事件的模式

# 第十三章 其它主题

# 依赖句柄

依赖句柄允许我们将两个对象的生存期耦合起来

  • 像其它 GC 句柄一样指向一个目标
  • 行为像一个弱句柄:即不能使目标一直保持存活

行为如下:

  • 句柄本身不会影响两个对象的生存期
  • 只要主对象存活,次要对象也存活

使用它的唯一方法是使用包装类 ConditionalWeakTable,其被组织为 Dictionary,其 key 存储主要对象,value 存储次要对象。

  • 注:此类的 key 是弱引用

依赖句柄的底层弱引用行为像长弱引用:即使主要对象终结仍然会保持维护主要对象和次要对象之间的关系,使其可以正确处理复活场景。

# 线程局部存储

线程局部存储 (TLS)

  • 其行为像一个全局变量,但数据是为每个线程单独存储的。

当前再 .NET 中有三种使用线程局部存储方法:

  • 线程静态字段:添加 ThreadStatic 属性标记后,就可以作为静态字段使用
  • 包装线程静态字段的帮助类: ThreadLocal<T> 类型
  • 线程数据插槽:在 Thread.SetDataThread.GetData 方法的帮助下使用

注:线程静态字段性能比线程数据插槽性能更好,且线程静态字段是强类型,而线程数据插槽始终对 object 进行操作。

# 线程静态字段

使用 ThreadStatic 标记常规静态字段即可,值和引用类型都可以用作线程静态字段。
注:若线程静态字段具有初始化程序,将只会在执行静态构造函数的线程上调用一次,会导致后续其它线程字段将保持其默认值。

  • 为克服类似问题,后续 .NET 提供了 ThreadLocal<T> 类以提供更好、更确定性的初始化行为。
  • 当然,也因此 ThreadLocal<T> 会比直接使用 ThreadStatic 方式性能差一些

# 线程数据插槽

使用非常简单,不过不建议使用,性能和 object 这种非强类型操作都很不妥当。

# 线程局部存储内部

有一个特殊的内存区域专用于每个线程自己的目的,在 Windows 中称为线程局部存储 (Thread Local Storage,TLS),在 Linux 中称为线程专用数据 (thread-specific data)

  • 但是这样的区域相当小,例如 Windows 保证每个进程仅有 64 个此类插槽可用,且最大数量不超过 1088 个,其无法保存数据本身

CLR 使用了 C++ 中使用线程局部存储方法,定义了一个 ThreadLocalInfo struct 类型的全局线程静态变量,其保留三个 CLR 内部数据地址:

  • 表示当前正在运行的托管线程的非托管 Thread 类的实例
  • 正在执行当前线程的 AppDomain 实例
  • ClrTlsInfo 结构的实例

当我们在 .NET 使用线程局部存储技术时,只存储了 ThreadLocalInfo 结构指针在 TLS 中,其它所有内容都驻留在 CLR 私有堆和 GC 堆中,即与常规静态变量实现方式类似:实例通常由堆分配,只是它们的引用存储在专用的常规对象数组中。

2140760369.jpeg

由于在编译时已经知道了类型的数量,因此专用的 Object [] 数组和静态 blob 都具有恒定的、预先计算的大小。
总的来说 TLS 仅用作对应数据结构的线程相关性的功能实现细节,它本身并没有加快任何速度 (甚至会有额外开销)

# 使用场景

  • 需要存储和管理线程敏感数据
  • 可以利用单线程相关性:
    • 日志记录或诊断
    • 缓存 (如 StreamBuilderCache)

注:使用线程静态变量显然不适合异步编程,因为异步方法的延续不能保证会在同一个线程执行。

  • 因此作为 ThreadLocal 的补充,AsyncLocal可用于在所有异步方法执行期间保留数据。

# 托管指针

简称 byref。
对象的引用实际上是一个类型安全指针 (地址),该指针始终指向对象 MethidTable 引用字段 (通常说指向对象的开头),有了对象引用,就有了整个对象地址。

  • 例如 GC 可通过常量偏移量快速访问标头
  • 通过 MethodTable 中存储的信息,字段地址也很容易计算

相比引用而言,托管指针可以定义为一种更通用的指针类型,它可以指向其它位置,而不是对象的开头。

  • 局部变量
  • 参数
  • 复合类型的字段
  • 数组元素

托管指针任然还是类型,有一个指向 System.Int32 对象的托管指针类型,强类型使它们比纯粹的非托管指针更安全。

  • 但托管指针只允许用于局部变量和参数签名
  • 由于这些限制,托管指针并没有直接暴露于 C#,以 ref 参数形式存在 (因此通常也称为 byref)

# ref 局部变量

可视为存储托管指针的局部变量

# ref 返回值

允许我们从方法中返回托管指针

  • 返回值的生存期必须超过方法执行范围
  • 例如不能返回方法中局部变量,可以是类的实例或静态字段、或传递给方法的参数

# 只读 ref 和 in 参数

用于控制 ref 变量存储变化能力

  • 对于值类型:保证该值不会更改
  • 对于引用类型:保证该引用不会被更改

注:如果在只读 ref 结构调用可修改值的方法也可以确保不被修改:因为这是通过防御性复制方法来实现。因为编译器分析调用方法是否确实会修改状态。
创建防御性副本是一个明显开销,可以通过将此类结构设为只读 (如果适用) 来避免防御性复制。

  • 编译器可以安全略过直接在传递值类型参数上创建防御性复制和调用方法的操作。

# ref 类型的内部

  • 指向堆栈分配对象的托管指针
  • 指向堆分配对象的托管指针
    • 指向内部字段 (内部指针) 如何保证其主对象没有其它引用时存活:有一个 brick 表和 plug 树,判断并使内部指针成为根 (但是有开销)

他们均需要对 GC 进行报告,以使 GC 能够检测到目标可达性

52284544.jpeg

# C# 中的托管指针 - ref 变量

ref (参数、局部变量、返回值) 都是围绕托指针的小型包装器,显然不应该被视为指针,而是属于变量。

避免复制数据 - 特别是大型结构 - 以类型安全方式

# 关于更多结构知识

只读结构

  • public readonly struct xxx
  • 避免防御性副本

ref 结构

  • public ref struct xxx
  • 编译器对其施加了很多限制,以使其只会被堆栈分配 (不能装箱)
    • 不能声明为常规结构或类的字段
    • 不能声明为静态字段
    • 不能装箱:不能分配 / 强转为对象、动态或任何接口类型,也不能用作数组元素
    • 不能用作迭代器、泛型参数
    • 不能在异步方法用作局部变量:因为会被闭包类装箱
    • 不能被 lambda 或局部函数捕获 (因为会被闭包装箱)
  • 特性:永远不会被堆分配、永远不会被多个线程访问到 (线程间传递堆栈地址非法)

# 固定大小缓冲区

将结构的一个字段定义为数组时,该字段只是对堆分配数组的引用 (而不是数组本身)

  • 固定大小缓冲区即是将整个数组嵌入结构中
  • 唯一限制:数组必须具有预定义的大小,类型只能是基元类型之一 (bool、byte、char、short、int、long、sbyte、ushort、uint、ulong、float、double)
  • 使用固定大小缓冲区的结构需要标记为 unsafe

固定大小缓冲区最长用于 P/Invoke 上下文中。

  • 也可以考虑将其用于通用代码,作为一种定义更密集数据结构的便捷方法,即使将此类结构作为泛型集合的一部分进行堆分配,生成代码也能提供更好的数据局部性。

1037927430.jpeg

注:还可以将它们与 stackalloc 组合使用,以创建包含『其它』数组的元素的堆栈分配数组。

# 对象 / 结构布局

数据对齐

  • 每种基元数据类型都有其自己首选的对齐方式 —— 存储其地址的值的倍数。
  • 通常,这种基元类型对齐方式与其大小相等
  • (CPU 访问未对齐数据需要更多指令)

在包含基元类型的复杂类型在布局这些字段时也需要考虑它们的对齐要求,因此会在字段之间引入填充

  • 复杂类型实例本身也应对齐,以确保其成为更复杂类型 (如数组) 一部分时其字段仍然对齐

MSDN 为有关对象的布局定义了三个规则:

  • 类型的对齐方式是其最大元素的大小或指定的打包大小 (以较小者为准)
  • 每个字段必须与其自身大小或类型的对齐方式 (以较小者为准) 对齐
  • 在字段之间添加填充以满足对齐要求

两种类型类别中字段布局设计决策:

  • 结构:默认情况具有顺序布局 (因为默认假定其会传递给非托管代码),这会引入填充并增加生成结构的大小
  • 类:默认情况下具有自动布局,字段会以最高效的方式重新排序

.NET 提供的控制字段布局方法:

  • LayoutKind.Sequential:顺序布局
  • LayoutKind.Auto:自动布局
  • LayoutKind.Explicit:显式手动布局

注 1:当为结构体添加托管的引用类型字段时,排列会自动更改为自动布局 (引用类型通常为第一个字段)。当该结构包含其它自动布局结构时,默认布局行为也会变为自动。
注 2:类和非托管结构的自动布局是无法更改的
注 3:上述特性提供了具有 Pack 参数的重载,即指定的打包大小,类型对齐也会以此为准

# 联合

手动指定布局时,故意让字段相互重叠,则称为可区分联合 (discriminated union)

# 字段对齐工具

  • ObjectLayoutInspector:用于检查对象内存布局,可打印类型布局信息。
  • Sharplab.io
  • WinDbg

# 非托管类型 (Unmanaged Type)

  • 14 种基元类型 + Decimal (decimal)
  • 枚举类型
  • 指针类型(比如 int*, long*)
  • 只包含 Unmanaged 类型字段的结构体

# 非托管约束

非托管类型是一直不是引用类型的类型,并且在任何嵌套级别都不包含引用类型字段。

  • 即上文提到过的不包含 (嵌套) 引用类型的结构体

借助非托管泛型约束,可以让编译器为我们检查非托管类型条件,可用于泛型方法和泛型结构类型。
使用:where T:unmanaged

# 非托管约束的作用

  • 可以使用 T 的指针 (也可以转换为 void*)
  • 可以使用 sizeof (T)
  • 可以对 T 使用 stackalloc

注:由于非托管约束意味着 T 是一个值类型,因此不需要固定即可获取参数的指针 (通过引用传递、或在结构实例方法中使用时,还是必须固定,因为可能是装箱的堆分配)
另外还可以借此更方便使用非托管内存类型。

# blittable 类型

blittable 类型被定义为托管和非托管代码在内存中都具有相同的表示形式。
非托管类型与 bilittable 类型几乎相同,不过后者比前者更为严格。因为有些值类型只是『有时是 bilttable』:

  • deceimal:二进制表示形式不够完善,因此不能采用非托管方面格式
  • bool:通常在托管和非托管方面都占用 1 字节,但有时在非托管方面占用会更大
  • char:通常占用 2 字节,但有时在非托管方面会更小或更大 (取决于编码)
  • DateTime:具有自动布局结构因此不能 blittable
  • Guid:内部表示取决于机器端

因此包含这种特殊值类型字段的结构是有效的非托管类型 (满足非托管泛型约束),但在 Interop marshal 意义上是不能 bilittable 的
总结:

  • 非托管类型 (以及非托管泛型约束) 主要用于通用编程,通常于 unsafe 上下文使用,对序列化等功能进行底层内存优化
  • Blittable 类型在 Interop marshal 处理场景中使用

更多知识:
.NET 的基元类型包括哪些?Unmanaged 和 Blittable 类型又是什么?

# 第十四章 高级技巧

# Span<T>

为值类型 (ref struct),可以表示各种形式的值的连续集合,可以像使用数组一样使用,并内置切片功能。

  • 由于其分配在堆栈上,因此完全没有堆分配开销
  • 编译器也能智能地处理封装到 Span<T> 中的数据的生存期 (如返回局部 stack 数据会报错,返回封装托管数组 (或非托管内存) 的 span 则被允许)

注:C# 不允许将 stackalloc 的执行结果赋值给一个已经定义的变量 (它只能赋值给正在初始化的变量)
注:其它比如还可以使用 string.AsSpan().Slice() 代替开销更大的 SubString

# 内部实现

在 .NET Core 2.1 之后运行时提供了一种模拟内部指针 (byref) 的功能,称为快速 span,在这之前则称为慢速 span (兼容版本)。

  • 注 1:目前 byref (内部指针) 不支持定义为字段 (即使是 byref 类型中也不行)
  • 注 2:快速和慢速之分,实际相差 25%,通常使用情况下只相差 12%~15%

# Memory<T>

Span<T> 存在种种限制,如无法存在于堆中。
Memory<T> 同样表示任意内存中一段连续区间,不过它既不是 byref 式类型,也不包含 byref 式实例字段。可用于封装如下数据:

  • arrayT[]
  • 字符串
  • 实现 IMemoryOwner<T> 的类型 (对生存期有控制)

可以将 Memory<T> 想象成一个能自由分配并传入传出的盒子,通常不会直接访问其存储内容,而是:

  • 从它生成 Span<T> 以供局部高效使用 (因此也称为 Span<T> 工厂)
  • 对于 Memory<char> 可以 ToString 生成字符串,其它类型 ToArray 生成数组
  • Span<T> 一样支持数据切片
  • 注:切片和生成 Span 都是非常高效的操作

注:不允许 Memory<T> 封装 stack 数据 (如 stackalloc 返回值)

# Unsafe 类

相比使用普通的不安全代码 (基于指针和 fixed 语句)

  • System.Runtime.CompilerServices.Unsafe 提供了一组泛型 / 底层功能以一种更安全的方式操作指针
  • 并暴露了一些 CIL 支持但 C# 不直接支持的功能

注:当然实际上它所做的操作仍然是不安全且危险的
其提供了大量方法,按功能分组如下:

  • 类型转换和重解释:在非托管指针和 ref 类型之间来回转换,或在任意两种 ref 类型之间转换
  • 指针运算:可以像操作普通指针一样对 ref 类型实例做加法和减法
  • 信息:获取各种信息,如两个 ref 类型实例的大小或字节差异
  • 内存访问:从任何位置写入或读取任何内容

注:使用 Unsafe 的方法并不需要标记为 unsafe
MemoryMarshal 辅助类

  • AsBytes:将任何基元类型 (结构) 的 Span<T> 转换为 Span<byte>
  • Cast:在两种不同基元类型 (结构) Span<T> 之间相互转换
  • TryGetArray、TryGetMrmoryManager、TryGetString:尝试将指定 Memory<T>ReadOnlyMemory<T> 转换成一种特定类型
  • GetReference:以 ref 返回值 (ref return) 方式返回底层 Span<T>ReadOnlySpan<T> 对象

对非托管内存的封装 (原生内存分配):

  • jemalloc.NET
  • Snowflake

Unsafe 原理:

  • 对受 IL 支持但 C# 不支持的底层操作的封装

# 面向数据设计

即基于最高效的内存访问目的来设计数据,与面向对象设计针锋相对:

  • 设计数据和功能时尽量实现循序内存访问,同时考虑 cacheline 的限制 (将最常用数据打包在一起) 和分层缓存得影响 (将尽可能多的数据保持在高层缓存中)
  • 设计类型和数据以及使用它们的算法时,使其易于并行化且无需高开销的同步锁

面向数据设计还可以进一步细分为两类:

  • 战术型面向数据设计:专注于『局部』数据结构,如最高效的字段布局或以正确的顺序访问数据 (可以很容易在面向对象应用程序应用)
  • 战略型面向数据设计:从架构层面专注于应用程序的高层设计

# 战术型设计

  1. 将类型设计成把尽可能多的关联数据容纳进首个 cacheline(例如托管类型自动内存布局将引用字段放置于对象起始位置)
  2. 将数据设计成可以填充进更高层级的缓存
  3. 将数据设计成易于并行化
  4. 避免非循序,特别是随机式内存访问

# 战略型设计

战略型设计需要程序员大幅度转变自己的思维

  1. 从 Array-of-structures 走向 Structures-of-arrays(值类型数组才能提供更好的数据局部性)
  2. Entity Component System
    • 实体 (Entity):一个具有标识符的简单对象,不包含任何数据货逻辑。定义实体的功能是通过向它添加或移除特定组件。
    • 组件 (Component):只包含数据而不包含逻辑的简单对象。组件包含的数据表示了它所代表的功能的当前状态。
    • 系统 (System):特定功能与特性的逻辑所在,系统对过滤后的实体列表逐个进行操作。
    • 注:过滤实体的效率对系统很重要,但只要管理得当,数据组件将被循序访问,获得良好的数据局部性和预取命中率。

# 未来特性

  • 可空引用类型
  • Piplines:替代 streams,具有更好性能和避免分配,大量使用了 Span 和 Memory。目前 Kestrel 是 Piplines 主要使用者之一。

# 第十五章 编程 API

# GC API

# 收集数据和统计

  1. GC.MaxGeneration:告知 GC 当前实现的最大代数
  2. GC.CollectionCount (Int32):告知自程序启动以来特定代的 GC 出现次数 (包含性)
  3. GC.GetGeneration:告知给定对象所属的代
  4. GC.GetTotalMemory:返回所有代中正在使用的字节数 (不包括碎片),即托管堆上所有托管对象的总大小 (非常耗性能)
  5. GC.GetAllocatedBytesForCurrentThread:返回当前线程到目前为止分配过的字节总数 (注:只考虑分配过多少数量,并不考虑垃圾回收后的)
  6. GC.KeepAlive:延长堆栈根存活性,使传递的参数在调用此方法时至少可以到达行 (从而影响生成的 GC 信息)
  7. GCSetting.LargeObjectHeapCompactionMode:例如设置为 CompactOnce,可以显式请求发生第一个完全阻塞 GC 时压缩 LOH
  8. GCSetting.LatencyMode
  9. GCSetting.IsServerGC

# GC 通知

只有阻塞垃圾回收才会引发此类通知:

  • GC.RegisterForFullGCNotification:如果满足完全阻塞 GC 条件将引发此通知 (该通知并不能保证将会发生完全 GC,只有条件达到足以进行完全 GC 的阈值才会发生)
  • GC.CancelFullGCNotification
  • GC.WaitForFullGCApproach:无限期等待 GC 通知
  • GC.WaitForFullGCComplete:无限期等待完全 GC 的完成

# 控制非托管内存压力

通知 GC 某些托管对象正在持有 (或释放) 一些非托管内存:

  • GC.AddMemoryPressure(Int64)
  • GC.RemoveMemoryPressure(Int64)

# 无 GC 区域

创建运行时尝试禁止 GC 的代码区域:

  • GC.TryStartNoGCRegion
  • GC.EndNoGCRegion

# 终结 (Finalization) 管理

  • GC.ReRegiterForFinalize
  • GC.SuppressFinalize
  • GC.WaitForPendingFinalizers

# CLR Hosting

# ClrMD

一组用于自检托管进程和内存转储的托管 API,提供与 WinDBG 的 SOS 相似功能不过可以用 C# 更方便使用。
注:如 Netext 和 SOSEX 这些 WinDbg 扩展就是围绕 ClrMD 的包装器。

# TraceEvent

# 自定义 GC

从 .NET Core 2.1 开始,垃圾回收与执行引擎之间的耦合已经松动很多,并引入本地 GC 的概念,意味着现在 GC 是可插拔的:我们可以通过设置单个环境变量来插入自定义 GC。
它允许完全替换 GC 的实现。