【.Net技术栈梳理】02-核心框架与运行时(GC管理)

文章目录

  • [1 内存管理(垃圾回收GC)](#1 内存管理(垃圾回收GC))
      • [1.1 GC核心目标和基本概念](#1.1 GC核心目标和基本概念)
      • [1.2 GC的工作原理与工作方式](#1.2 GC的工作原理与工作方式)
      • [1.3 如何更好地使用 GC](#1.3 如何更好地使用 GC)

1 内存管理(垃圾回收GC)

.NET 的垃圾回收器(Garbage Collector, GC)是 .NET 内存管理的核心,理解其工作原理对于编写高性能、高稳定性的应用程序至关重要。

1.1 GC核心目标和基本概念

  • 目标

GC的存在主要是为了自动化内存管理,解决两个手动内存管理的经典难题:

  1. 内存泄露:忘记释放不再使用的内存
  2. 悬空指针:释放了仍在使用的内存
  • 基本假设:代际假说

GC的设计基于一个对软件行为观察得出的强大假说----代际假说

  • 对象越新,其生命周期越短。例如:在方法内部创建的局部变量很可能在方法结束后就无人引用了。
  • 对象越老,其生命周期越长。例如:全局的单例对象、缓存等,可能会从程序开始一直存活到结束。

这个假说经过长期验证,在绝大多数程序中都非常准确,是.NET GC分代收集的基础

  • 关键概念:托管堆

.NET中的对象实例(new关键字创建的引用类型对象)分配在托管堆 上。这与C++的malloc或new在原生堆上分配有本质区别。托管堆是由CLR直接管理的一块连续内存区域,GC负责其分配和回收

1.2 GC的工作原理与工作方式

1.分代收集

基于代际假说。.NET GC 将托管堆上的对象分为三代:

  • 第0代 :新创建的对象。这一代的大小通常很小(几百KB到及MB),大多数对象在第0代就会被回收。一次专注于第0代的回收速度非常快。

  • 第1代:从第0代回收中"幸存"下来的对象。它可以看作是第0代合第2代之间的缓冲区。其大小比第0代大,但是比第2代小。

  • 第2代:经历多次GC回收仍然存活的长生命周期对象。大型对象(>=85000字节)也会直接分配在大对象堆上,而LOH是按第2代来管理的。

工作流程

  1. 对象分配 :新对象分配在第0代。如果第0代已满,则触发一次第0代GC
  2. 第0代GC
    • GC 开始标记:从根对象(静态字段、局部变量、CPU 寄存器等)开始,遍历所有可达的对象图。
    • 存活的对象压缩(移动到第 1 代区域的起始端,消除内存碎片),并更新所有对这些对象的引用地址。
    • 第 0 代被清空,准备接收新对象
  3. 第 1 代 GC :当第 1 代也被填满时,会触发一次 第 1 代 GC。这个过程与第 0 代类似,但会同时回收第 0 代和第 1 代。在第 1 代 GC 中幸存的对象会被提升到第 2 代。
  4. 第 2 代 GC :当第 2 代被填满时,会触发一次 Full GC,即回收所有三代(包括大对象堆)。这是最耗时、最昂贵的操作,因为它需要检查整个托管堆中的所有对象。

这种分代设计极大地优化了性能,因为 GC 大部分时间都在处理小而新的第 0 代,而很少去处理庞大而古老的第 2 代。

2.标记与压缩

这是 GC 回收过程的核心步骤:

  • 标记:GC 遍历所有"根",找出所有仍然被引用的存活对象,并标记它们。所有未被标记的对象就是"垃圾"。
  • 清除:回收垃圾对象占用的内存。
  • 压缩:(并非所有 GC 都触发) 为了消除内存碎片,GC 会将存活的对象移动到一起,使其在内存中连续排列。这样,下一次分配新对象就可以快速地在空闲内存的末尾进行(通过简单的指针加法)。压缩后,所有对象的引用地址都会被更新。

3.GC 模式

.NET GC 提供了不同的模式来适应不同特点的应用(如客户端GUI应用 vs. 服务器后端服务)。

  • 工作站 GC

    • 设计目标:低延迟,与用户交互的应用程序(如 WPF, WinForms)。保证 GC 不会"冻结"UI 过长时间。
    • 特点:GC 在触发它的用户线程上运行,默认是并发的(后台进行一个并发的第 2 代回收,尽量减少对主线程的阻塞)。
  • 服务器 GC

    • 设计目标:高吞吐量,服务器端应用程序(如 ASP.NET Core Web API)。
    • ** 特点**:
      • 为每个逻辑 CPU 核心创建一个独立的托管堆和一个专门的 GC 线程。
      • 所有 GC 线程并行工作,可以更快地完成一次完整的 GC。
      • 消耗更多内存(每个堆都需要预留空间),但吞吐量极高。
    • 通常在 *.csproj 文件或 runtimeconfig.json 中显式配置:

    <PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection> </PropertyGroup>

  • 后台 GC:

    • 这是工作站 GC 的增强特性(.NET Framework 4.0+, .NET Core 始终启用)。
    • 允许在进行第 2 代 GC 的同时,仍然可以处理第 0 代和第 1 代的 GC 请求。这极大地减少了长时间的 Full GC 造成的阻塞。

1.3 如何更好地使用 GC

理解 GC 原理的最终目的是为了写出对 GC 更"友好"的代码,避免不必要的性能开销。

  1. 基本准则:减少压力

GC工作的频率和时长直接取决于你分配新对象的数量和频率。

  • 核心目标:减少不必要的对象分配,尤其是短命的、在第0代就会死亡的对象
  1. 具体实践
    1. 对象池化

      • 场景:对于创建成本高、生命周期短且频繁创建/销毁的对象(如 HttpClient? 实际上 HttpClient 应重用,但这里是另一个例子如数据库连接、特定自定义对象)。
      • 做法:使用 Microsoft.Extensions.ObjectPool 或自定义池。不从池中 new 新对象,而是从池中借用一个已存在的对象,用完后归还。这完全避免了 GC 开销。
      • 实例:ArrayPool< T> 用于租赁和归还数组,是池化的经典应用
    2. 使用值类型

      • 场景: small, short-lived data structures.
      • 做法:使用 struct 而不是 class。值类型分配在栈上(或作为引用类型的一部分内联在堆上),方法结束时栈自动清理,无需 GC 介入。
      • 注意:不要滥用。值类型有装箱拆箱开销和复制语义,适用于小尺寸(通常小于 16 字节)、不可变、生命周期很短的情况。
    3. 避免大对象

      • 任何 >= 85,000 字节的对象会直接进入大对象堆,由第 2 代管理。LOH 的回收成本高且不压缩,容易产生碎片。应尽量避免创建大型对象(如大数组、大字符串)。
    4. 及时释放非托管资源

      • GC 只管理托管内存。对于文件句柄、数据库连接、网络套接字等非托管资源,GC 无能为力。
      • 必须实现 IDisposable 接口,并在 using 语句或 try/finally 块中调用 Dispose() 方法以确保资源被及时释放。
      • 模式:Dispose Pattern。
    5. 避免不必要的终结器

      • 终结器是 GC 在回收对象时,如果该对象有终结器,则不会立即回收,而是将其放入一个队列,由另一个线程调用其终结器,这会导致对象晋升到下一代,并延迟实际的内存回收,极大地增加 GC 压力。
      • 只有在你直接持有非托管资源时才需要实现终结器,作为一道安全网(如果开发者忘了调用 Dispose)。对于绝大多数包装了非托管资源的类,应使用 SafeHandle 的派生类,它已经为你正确地实现了终结器。
    6. 谨慎处理事件和委托

      • 事件处理程序会形成强引用。如果一个长生命周期对象订阅了一个短生命周期对象的事件,会导致短生命周期对象无法被回收(相当于被长生命周期对象引用着)。
      • 记得在不需要时取消订阅,否则会造成内存泄漏。
    7. 使用合适的集合和容量

      • 像 List< T>, Dictionary<TKey, TValue> 这样的集合在内部使用数组。如果预先知道大致容量,应在构造函数中指定初始容量,避免内部数组频繁扩容和复制,从而减少垃圾产生。
    8. 使用性能分析工具

      • Visual Studio Diagnostic Tools:其中的内存分析器可以帮你查看内存分配、存活的对象、找出内存泄漏的根源
      • PerfView:强大的底层性能分析工具,可以深入分析 GC 事件、停顿时间、各代回收频率等
      • dotTrace / JetBrains Rider:提供出色的内存和性能分析功能。

总结

.NET 的 GC 是一个高度工程化的复杂系统,它通过分代收集、标记-压缩和多种工作模式,在自动化、性能和延迟之间取得了出色的平衡。

  • 理解其原理:知道分代、堆的结构、GC 触发时机。
  • 减少其工作量:核心是减少不必要的、尤其是短命的托管对象分配(池化、值类型、控制集合大小)。
  • 管理好非托管资源:严格遵循 IDisposable 模式。
  • 使用工具验证:不要猜测,用性能分析工具来定位真实的内存问题。

遵循这些原则,就能写出对 GC 友好、内存高效且性能卓越的 .NET 应用程序。


.NET 垃圾回收器中各代的大小并非固定不变,而是一个复杂的、由CLR动态调整的参数。它的界定方式是其高性能设计的核心所在

第 0 代、第 1 代和第 2 代的大小不是固定的 。它们由 CLR 的垃圾回收器根据应用程序的分配行为、负载和运行模式(工作站 vs. 服务器)动态调整

由于大小是动态的,我们无法通过代码获取一个精确的、永恒不变的值。但我们可以通过诊断工具在特定时刻实时观察它们。

  • 方法 1:使用 Visual Studio Diagnostic Tools
  1. 在 Visual Studio 中运行你的应用程序。
  2. 点击 Debug > Windows > Show Diagnostic Tools。
  3. 在 Diagnostic Tools 窗口中,选择 Memory Usage 标签页。
  4. 点击 Take Snapshot 按钮捕获一个内存快照。
  5. 在快照详情中,你可以看到 Heap Size,它会详细列出第 0/1/2 代和大对象堆的当前大小。
  • 方法 2:使用 GC.GetGCMemoryInfo API (.NET 5+)
    这是一个编程接口,可以获取当前GC的内存信息,包括各代的大小
csharp 复制代码
GCMemoryInfo gcInfo = GC.GetGCMemoryInfo();

// 注意:这里获取的是'Generation'的大小,不是'代'的预算大小,但紧密相关。
// 它反映了上次GC后,该代中存活对象占用的空间,可以近似代表代的大小。
long gen0Size = gcInfo.GenerationInfo[0].SizeAfterBytes;
long gen1Size = gcInfo.GenerationInfo[1].SizeAfterBytes;
long gen2Size = gcInfo.GenerationInfo[2].SizeAfterBytes;
long lohSize = gcInfo.GenerationInfo[3].SizeAfterBytes; // LOH 是 generation 3

Console.WriteLine($"Gen0: {gen0Size / 1024} KB");
Console.WriteLine($"Gen1: {gen1Size / 1024} KB");
Console.WriteLine($"Gen2: {gen2Size / 1024} KB");
Console.WriteLine($"LOH: {lohSize / 1024} KB");

注意:这个API返回的是更底层的细节,SizeAfterBytes 表示的是上次GC后该代中存活对象的大小,这个值会非常接近GC为该代设定的"预算"大小。

  • 方法 3:使用 PerfView 等高级分析器
    PerfView 可以捕获GC事件的所有细节,包括每次GC前后各代的大小变化,是进行深度GC性能分析的终极工具。

思考各代大小时,应记住它们是动态的性能调优参数,而不是静态的配置值。GC 的智能之处就在于它替你完成了绝大部分复杂的内存调整工作。开发者的任务则是通过减少不必要的分配(尤其是短命对象)来配合 GC 的工作。


相关推荐
Scott9999HH10 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社11 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海11 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖12 小时前
JDK 26 新特性详解
java·开发语言
马优晨13 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区14 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大15 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai15 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜15 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa16 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio