当前位置: 首页 > news >正文

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

文章目录

  • 1 .NET Runtime(CLR-公共语言运行时)
    • 1.1 中间语言 IL
      • 1.1.1 从源代码到通用中间语言(IL)
      • 1.1.2 运行时加载:CLR登场
      • 1.1.3 核心步骤:即时编译 (JIT Compilation)
      • 1.1.4 执行与内存管理(GC)
      • 1.1.5 演进与高级模式:分层编译与 AOT
  • 2 内存管理(垃圾回收GC)
      • 2.1 GC核心目标和基本概念
      • 2.2 GC的工作原理与工作方式
      • 2.3 如何更好地使用 GC

核心框架与运行时是.NET的基石,决定了能开发什么类型的应用已经如何运行。

1 .NET Runtime(CLR-公共语言运行时)

负责执行编译后的代码(中间语言,IL)、内存管理(垃圾回收GC)、异常处理、线程管理等。是所有.Net应用的引擎

1.1 中间语言 IL

.Net运行时(CLR)执行编译后的代码(中间语言,IL)是一个核心过程,理解这个过程就能明白跨平台、安全性、高性能等特性的基础。

整个过程可以概括为一下关键阶段

  1. 编写源代码与编译为IL
  2. 分发与部署(包含IL的程序集)
  3. 运行时加载与即时编译(JIT Compilation)
  4. 执行本地代码
  5. 优化与高级特性(分层编译、AOT)

1.1.1 从源代码到通用中间语言(IL)

当使用 C#、F# 或 VB.NET 编写代码并执行 dotnet build 时,发生的事情与 C/C++ 这样的原生语言完全不同

  • C/C++ (原生编译):编译器直接将源代码编译为针对特定 CPU 架构(如 x86, ARM)和操作系统的本地机器码。这个代码无法在其他平台上运行
  • .NET (托管编译):编译器(如 Roslyn for C#)会将源代码编译为一种称为 中间语言 (IL) 或 通用中间语言 (CIL) 的字节码。同时,它还会生成丰富的元数据(描述代码中的类型、成员、引用等信息)
  • IL是什么

可以把IL想象成一种高度抽象、与特定CPU无关的“汇编语言”。它比高级语言更底层,但是比真正的机器码更高级。它包含了ldloc(加载本地变量)、add(相加)、call(调用方法)这样的指令

  • 为什么这样做?

关键优势:跨平台和语言互操作性。IL是一种统一的、标准的输出格式。无论使用的事C#还是F#,最终都变成了IL。这使得.NET运行时只需要理解IL这一种语言,就能运行所有.NET语言编写的程序。同时,因为IL不是特定于某个平台的,所以同一个IL程序集(.dll或.exe)可以分发到任何有相应.NET运行时(CLR)的平台上(Windows、Linux、macOS)

1.1.2 运行时加载:CLR登场

当运行一个.NET程序时,操作系统会启动.NET运行时(CLR)。CLR的程序集加载器会负责找到并加载程序集(以及它所依赖的所有程序集)。加载后,CLR会读取其中的元数据和IL代码,为执行做准备。

1.1.3 核心步骤:即时编译 (JIT Compilation)

这是最神奇、最核心的一步,CLR不会直接“解释”执行IL(像早期的Java或Python那样)。相反,它使用一个名为JIT编译器(Just-In-Time Compiler)的组件

JIT编译器的工作流程如下:

  1. 按需编译:当一个方法(函数)第一次被调用时,JIT编译器才会开始工作。CLR不会在程序启动时就把所有IL都编译成本地代码,这避免了不必要的启动延迟。
  2. 读取IL:JIT编译器从已加载的程序集中获取该方法的IL代码。
  3. 验证:在编译之前,JIT会执行一个重要的验证过程。它会检查IL代码是否是类型安全的(例如:不会错误地将一个整数当做对象引用来使用)。这个步骤是.NET内存安全和安全沙箱的基石,它能组织大量潜在的内存损坏漏洞。
  4. 编译为本地代码:验证通过后,JIT编译器将IL代码动态地编译成当前所在平台本地机器码(x86、x64、ARM等)。这个过程考虑了当前的CPU和操作系统环境。
  5. 存储和执行:编译生成的本地机器码被存储在内存中的一块特定的区域(通常称为JIT代码堆)。然后CLR修改该方法的方法表,使其条目指向这块新生成的本地代码。最后,程序执行这个刚刚编译好的、极其高效的本地代码。

JIT的优势

  • 跨平台
    同一个IL包,在Windows上JIT编译为x86代码,在Linux上编译为x64代码,在Raspberry Pi上编译为ARM代码。

  • 性能优化
    JIT编译器可以进行运行时优化。它可以根据程序运行的实际环境进行优化。例如,如果它检测到运行程序的CPU支持特定的指令集(如AVX2),它就可以生成使用这些指令的更高效的代码。静态编译器(如C++)在编译时无法知道程序最终会运行在什么CPU上,因此无法做到这一点。

  • 节省内存
    只有真正被执行到的代码才会被编译和加载到内存中。

1.1.4 执行与内存管理(GC)

代码已经是以本地机器码的形式在 CPU 上直接执行了,速度非常快。
在执行过程中,CLR 的另一个核心组件——垃圾回收器 (Garbage Collector, GC)——会持续工作。它负责自动分配和释放内存。当对象不再被引用时,GC 会自动回收它们占用的内存,开发者无需(也不能)手动释放。这消除了内存泄漏和悬空指针等常见问题。

1.1.5 演进与高级模式:分层编译与 AOT

最初的 JIT 编译策略是“一次性编译”,但现代 .NET(.NET Core 3.0+)引入了更先进的策略:

  1. 分层编译 (Tiered Compilation)
  • 第一层 (快速 JIT):当一个方法第一次被调用时,JIT会快速地进行编译,生成优化程度较低但编译速度极快的代码。目标是尽快让程序跑起来
  • 第二层 (优化 JIT):如果发现某个方法被频繁调用(成为“热路径”),CLR会在后台异步地启动一个优化版本的JIT编译器,重新编译该方法,生成高度优化的、更快的本地代码。之后对该方法的调用就会切换到优化版本上。
  • **好处:**完美平衡了启动速度和运行速度。
  1. 预先编译 (AOT - Ahead of Time)
  • 虽然JIT很棒,但是它的编译过程仍然会在程序运行时产生一些开销(CPU和内存)。对于某些场景(如启动速度极致的App、命令工具),我们希望消除这个开销
  • Native AOT:.NET提供了Native AOT编译模式。它在发布时就直接将IL代码编译为本地可执行文件,完全不需要在目标机器上安装.NET运行时,也没有JIT编译阶段
  • 结果:生成的文件更大,启动速度极快,但失去了JIT的运行时优化能力。.NET 8和更高版本对Native AOT的支持已经非常完善

总结与类比

步骤.Net(托管)Java传统原生(C/C++)
编译源代码 -> 中间语言 (IL)+ 元数据源代码 -> 字节码 (.class)源代码 -> 本地机器码 (.exe)
分发包含 IL 的程序集(跨平台)包含字节码的 JAR 文件(跨平台)特定平台的二进制文件
执行CLR + JIT 编译为本地代码并执行JVM + JIT 编译为本地代码并执行操作系统直接加载执行

可以把一个 .NET 程序想象成:

  • IL 是一份标准化的、与烹饪设备无关的菜谱
  • CLR 是一位厨师(JIT 编译器)和一个厨房(运行时环境)
  • 厨师在接到订单(方法调用)时,根据手头的厨具(CPU 架构)和食材(环境),现场(Just-In-Time) 将菜谱翻译成具体的烹饪步骤(本地机器码)并做菜(执行)。

这种方式既保证了菜谱(程序)的通用性,又能让每位厨师(不同平台上的 CLR)利用自己厨房的最优条件做出最好的菜。

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

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

2.1 GC核心目标和基本概念

  • 目标

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

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

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

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

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

  • 关键概念:托管堆

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

2.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 造成的阻塞。

2.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的内存信息,包括各代的大小
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 3Console.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 的工作。



文章转载自:

http://L6SXcadI.hqjtp.cn
http://1v51hqvD.hqjtp.cn
http://h2kz0tUm.hqjtp.cn
http://6kNe4eqe.hqjtp.cn
http://UcOxuFnH.hqjtp.cn
http://LFyeIsgC.hqjtp.cn
http://zh2po530.hqjtp.cn
http://TwXl3jrm.hqjtp.cn
http://YTzwrQMQ.hqjtp.cn
http://4DRUYUAP.hqjtp.cn
http://DYM6MY4D.hqjtp.cn
http://abuXvrQE.hqjtp.cn
http://io9Qtz6f.hqjtp.cn
http://StC2eCHW.hqjtp.cn
http://KbXXoqQl.hqjtp.cn
http://VuHyrDcg.hqjtp.cn
http://r5Hcs0hm.hqjtp.cn
http://8aRrVrHG.hqjtp.cn
http://Lk0B1qff.hqjtp.cn
http://OAnuCWT9.hqjtp.cn
http://BOEMsXQS.hqjtp.cn
http://H0whBpIy.hqjtp.cn
http://bgPaIgQ1.hqjtp.cn
http://irtKthxW.hqjtp.cn
http://RwY0kBnl.hqjtp.cn
http://OQSf4T74.hqjtp.cn
http://Pi8GEvWh.hqjtp.cn
http://TIn3HkW8.hqjtp.cn
http://OdaQ3ztc.hqjtp.cn
http://WFicwxMp.hqjtp.cn
http://www.dtcms.com/a/371688.html

相关文章:

  • 简述ajax、node.js、webpack、git
  • Java安全体系深度研究:技术演进与攻防实践
  • Drupal XSS漏洞复现:原理详解+环境搭建+渗透实践(CVE-2019-6341)
  • Mybatis常见问题
  • Python基础语法篇:布尔值是什么?True 和 False 的实际用途
  • FMI(Functional Mock-up Interface,功能模型接口)
  • macOS中设置环境变量的各文件及作用域
  • Python+DRVT 从外部调用 Revit:批量创建楼板
  • 课前准备--解码乳腺癌进展:单细胞基因组与转录组的联合分析
  • 机器学习中的损失函数是什么
  • P5019 [NOIP 2018 提高组] 铺设道路
  • 【 苍穹外卖 | Day2】
  • 简单的说一说前端开发语言React
  • 跨域解决方案——CORS学习了解
  • leetcode 1304. 和为零的 N 个不同整数 简单
  • LeetCode 面试经典 150 题:合并两个有序数组(双指针解法详解)
  • Nestjs框架: 基于策略的权限控制(ACL)与数据权限设计
  • Go语言实战案例-实现简易定时提醒程序
  • 如何在项目中使用 Claude 记忆库系统(二开场景指南)
  • Matlab Simulink中的一些记录
  • 在Word和WPS文字的表格中快速输入连续的星期、月、日
  • Linux 周期性用户作业计划:crontab
  • Flink TaskManager日志时间与实际时间有偏差
  • 综合案列(SQLpymysql)
  • 得物后端二面
  • v$lock TS lock id1 用于发现Oracle pdb不能关闭的sid
  • Lenovo联想YOGA Pro 16 IAH10 2025款笔记本电脑(83L0)开箱状态预装OEM原厂Win11系统
  • 硬件-电容学习DAY3——钽电容制造全解析:从粉末到精品的奥秘
  • word2vec模型案例
  • Python将md转html,转pdf