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

【层面二】.NET 运行时与内存管理-01(CLR/内存管理)

文章目录

  • 1 .NET 运行时(CLR)核心
    • 1.1 核心比喻:CLR 是一个“托管国度”的政府
    • 1.2 CLR 的四大核心支柱
      • 1.2.1 支柱一:通用类型系统 (CTS) - 国家的“官方语言”
      • 1.2.2 支柱二:公共语言规范 (CLS) - 国家的“最低交流标准”
      • 1.2.3 支柱三:虚拟机与执行引擎 - 国家的“司法与执法系统”
      • 1.2.4 支柱四:核心公共服务 - 国家的“公共设施”
    • 1.3 程序的完整执行旅程:从源代码到运行
    • 1.4 总结:CLR 的本质与价值
  • 2. 内存管理与垃圾回收(GC)
    • 2.1 内存结构
      • 2.1.1 托管堆:公司的公共仓库
      • 2.1.2 栈
      • 2.1.3 大对象堆(LOH)
    • 2.2 垃圾回收(GC)

.NET 运行时与内存管理

1 .NET 运行时(CLR)核心

1.1 核心比喻:CLR 是一个“托管国度”的政府

为了理解 CLR 的庞大职责,我们将其想象成一个国家的中央政府:

  • 你的 .NET 代码:是这个国家的公民和法律法规(指令)。

  • 操作系统(如 Windows, Linux):是物理世界,提供最基础的资源(土地、水电)。

  • CLR(中央政府):它建立在物理世界之上,负责管理所有公民(代码执行),并为其提供一套强大的公共服务和严格的安全规则,让公民可以高效、安全地生活和工作,而无需直接面对物理世界的复杂性和危险性。

这个“托管”(Managed)的核心含义就是:由 CLR 这个“政府”来为你管理内存、安全、线程等复杂繁琐的事务,让你(开发者)可以专注于业务逻辑本身。

1.2 CLR 的四大核心支柱

CLR 的职能可以归纳为四大核心支柱,它们共同协作,构成了托管环境。

1.2.1 支柱一:通用类型系统 (CTS) - 国家的“官方语言”

  • 是什么:CTS 是一套所有 .NET 语言都必须遵守的关于类型的定义、行为和关系的规范

  • 目的:确保在一种语言中定义的类型(如 C# 的 class)可以在另一种语言中(如 F#)无缝使用。它定义了所有类型最终都派生自 System.Object,规定了什么是类、接口、委托、值类型、引用类型等。

  • 比喻:CTS 就像是国家的官方语言和书写规范。无论是来自 C# 省、F# 省还是 VB.NET 省的公民,都必须使用这套统一的语言和交流规则,才能在这个国家内无障碍沟通协作。

1.2.2 支柱二:公共语言规范 (CLS) - 国家的“最低交流标准”

  • 是什么:CTS 的一个子集。它定义了所有 .NET 语言都必须支持的最小功能集。

  • 目的:确保开发者编写的代码可以被任何其他 .NET 语言使用。如果你希望代码是“符合 CLS 的”,就应该避免使用某些语言特有的特性(如 C# 的 uint)。

  • 比喻:CLS 就像是官方语言下的最低交流标准。一个产品只要满足这个最低标准,就可以在全国所有地区销售和使用。开发者可以选择只使用这些特性,来保证最大的互操作性。

1.2.3 支柱三:虚拟机与执行引擎 - 国家的“司法与执法系统”

这是 CLR 最核心的部分,负责“执行法律”(你的代码)。它本身又包含几个关键组件:

  1. 类加载器 (Class Loader)

    • 职责:在需要时(例如第一次使用一个类),负责查找、加载程序集(.dll),并从中读取元数据,在内存中创建类的内部数据结构(如方法表)。

    • 比喻:政府的档案管理部门。当需要执行某条法律(使用某个类)时,它负责去档案馆(程序集)中找到对应的法律条文(元数据和IL),并准备好供法官(JIT编译器)使用。

  2. 即时编译器 (JIT Compiler)

    • 职责:将中间语言 (IL) 编译成本地 CPU 架构的原生机器码。它按需进行,方法只有在第一次被调用时才会被编译。

    • 工作原理:

      • 验证:首先对 IL 代码进行严格的类型安全验证,防止危险的操作(如非法内存访问)。这是托管环境安全性的基石。

      • 编译:将验证通过的 IL 编译为高度优化的本地代码。

      • 存储:将编译好的本地代码存储在内存中,下次调用直接执行。

    • 优势跨平台(一份IL,处处编译运行)和运行时优化(可以根据当前机器的CPU特性进行优化)。

    • 比喻:最高法院的法官。他负责解读通用的法律条文(IL),并根据本地的具体情况(CPU架构),将其翻译成可执行的、具体的判决指令(本地机器码)。他的解读(编译)是具有权威性(优化过)的。

  3. 垃圾回收器 (Garbage Collector - GC)

    • 职责:自动管理托管堆的内存分配与释放。它追踪对象的引用,定期回收不再使用的对象所占用的内存,并压缩内存以消除碎片。

    • 比喻:国家的环保与资源回收部门。它自动跟踪所有物资(对象)的使用情况,定期回收废弃的物资(垃圾对象),并整理仓库(堆)以腾出连续的空间,确保资源的高效利用。开发者无需(也不能)手动“扔垃圾”。

1.2.4 支柱四:核心公共服务 - 国家的“公共设施”

CLR 提供了一系列所有“公民”都能使用的强大服务:

  1. 类型安全与安全性:

    • 代码访问安全 (CAS):根据代码的来源(如网络、本地磁盘)赋予其不同的信任级别和权限。(在现代 .NET 中有所演变,但思想仍在)。

    • 验证:JIT 编译前的验证确保了代码不会执行非法操作,从根本上杜绝了缓冲区溢出等许多安全漏洞。

  2. 线程管理:

    • 线程池:CLR 管理着一个线程池,高效地重用线程,避免了频繁创建和销毁线程的巨大开销。ThreadPool.QueueUserWorkItem 和 Task 都基于此。
  3. 异常处理:

    • 提供了一套结构化的、跨语言的异常处理机制。当异常被抛出时,CLR 会中断当前流程,遍历调用栈,寻找合适的 catch 块来处理异常。
  4. 互操作性:

    • 平台调用 (P/Invoke):允许托管代码调用原生 C/C++ 编写的库(DLLs)。

    • COM Interop:允许 .NET 与传统的 COM 组件进行交互。

1.3 程序的完整执行旅程:从源代码到运行

让我们跟踪一行 C# 代码的完整生命周期,看看 CLR 是如何参与其中的:

JIT编译核心过程
将机器码存入内存
并更新方法表指针
验证IL的
类型安全性
编译为当前CPU架构的
原生机器码
C#/F#/VB.NET 源代码
语言编译器
Roslyn等
生成
程序集
内含IL代码与元数据
分发与部署
CLR加载程序集
方法是否为首次调用?
直接执行
已编译的原生代码

这个流程图的核心在于:CLR 通过 JIT 编译和托管环境,在开发效率(自动内存管理、跨平台、类型安全)、执行性能(JIT 优化、高效内存分配)和安全性之间取得了非凡的平衡。

1.4 总结:CLR 的本质与价值

CLR 的本质是一个提供了强大公共服务和严格安全管理的托管执行环境(虚拟机)。

它的核心价值在于:

  1. 提高开发效率与可靠性:通过自动化管理(尤其是内存管理)和强制安全措施(类型验证),将开发者从最容易出错、最繁琐的任务中解放出来,大大减少了内存泄漏、指针错误和安全漏洞。

  2. 实现语言互操作性:通过 CTSCLS,让不同的 .NET 语言可以无缝协作,允许团队为不同任务选择最合适的语言。

  3. 提供一致的编程模型:无论应用程序是 Windows 窗体、ASP.NET Web 应用还是移动应用,它们都使用同一套由 CLR 提供的基类库和服务(如文件 I/O、网络通信、集合类)。

  4. 简化部署与提升可移植性:程序集是自描述的,避免了 DLL Hell。通过 JIT 编译,“一次编写,多处运行”在很大程度上得以实现。


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

2.1 内存结构

核心比喻:公司的办公空间
想象一下,我们把整个 .NET 程序的内存空间比作一家公司的办公场地:

  • 进程:一整栋办公大楼。这是你的应用程序运行的独立空间。

  • 内存:大楼里的所有可用空间和房间

  • 线程:大楼里的员工,他们在空间里走动和工作。

现在,我们来看看这栋大楼里三个最关键的功能区。


2.1.1 托管堆:公司的公共仓库

托管堆是所有线程共享的、由垃圾回收器(GC)自动管理的内存区域。

特性与工作原理:

  1. 动态分配:分配内存像是在一个巨大的仓库里找一个空位放新货。这比开抽屉慢,因为它需要查找可用的空闲内存块。

  2. 不确定的生命周期:货物(对象)在仓库里的存活时间不固定。只要还有人在用这个货物(即存在对该对象的引用),它就会一直留着。当完全没人用时,它也不会被立刻清理,而是要等待仓库管理员(GC) 定时来巡检和清理。

  3. 存储内容:所有引用类型对象的实例本身。当你写 new MyClass() 时,MyClass 对象的所有内容(它的字段等)都存放在这里。

  4. 代际优化:托管堆被 GC 分为 3 代(Gen 0, Gen 1, Gen 2),基于“代际假说”(绝大多数对象都是短命的)。

    • Gen 0:新货临时存放区。所有新对象都先放这里。管理员非常频繁地检查这里,发现没用的货物(垃圾)就立刻清掉。速度极快

    • Gen 1:中转区。从 Gen 0 清理中幸存下来的货物移到这里。管理员检查这里的频率较低。

    • Gen 2:长期仓储区。从 Gen 1 幸存下来的“老员工”货物放在这里。管理员很少来检查,但一旦来,就是一次大清理(Full GC),耗时较长。

  5. 压缩:GC 在清理垃圾后,会对幸存的对象进行压缩(挪动位置),以消除内存碎片,腾出连续的空间。这就像整理仓库,把散落的货物堆紧,空出一大块完整的区域。

比喻总结:托管堆就像公司的公共仓库。空间巨大,存放所有大家共享的物资(对象)。存取速度尚可,但清理工作由专门的管理员(GC)在不确定的时间进行,而且清理过程本身(尤其是整理压缩)会耗费一些时间。

2.1.2 栈

栈是每个线程私有的、后进先出(LIFO)的高速内存区域。

特性与工作原理:

  1. 极致速度:分配和释放内存就像打开和关上抽屉一样快,仅仅是通过移动一个指针(栈指针)来实现。CPU 对此有硬件级别的直接优化。

  2. 严格的生命周期:抽屉里的东西(数据)的生命周期与员工正在处理的当前任务(方法调用) 完全绑定。

    • 任务开始(方法被调用):员工拉开抽屉,把任务需要的工具(局部变量、方法参数)放进去。

    • 任务完成(方法返回):员工直接关上整个抽屉!里面的所有东西瞬间、自动地被清理掉。无需任何垃圾回收器介入。

  3. 存储内容:

    • 值类型:如 int, double, bool, char, struct。这些数据直接存放在抽屉里。

    • 引用:引用类型的变量(如 MyClass obj)本身也放在抽屉里。但这个变量不是对象本身,而是一个地址条,指向堆中对象实际存放的位置。

  4. 线程安全:每个员工(线程)都有自己的专属抽屉(栈),互不干扰,所以绝对安全。

  5. 大小限制:栈空间通常较小(默认每个线程 1 MB)。如果递归太深或分配过大的值类型结构(如一个超大的 struct),会导致 StackOverflowException——就像抽屉被塞爆了。

比喻总结:栈就像员工的个人办公桌抽屉。空间小,但存取极快,私密性强,而且清理起来毫不费力(方法结束即自动清理)

2.1.3 大对象堆(LOH)

存放大型设备的特殊仓库区,大对象堆是托管堆的一部分,专门用于存放大型对象(通常 >= 85,000 字节)。

特性与工作原理:

  1. 准入标准:对象大小 >= 85,000 字节。常见的有大数组(byte[])、大字符串、或某些复杂的大对象。

  2. 特殊待遇

    • 直接进入 Gen 2:大对象直接放入“长期仓储区”,跳过 Gen 0 和 Gen 1。因为移动它们的成本太高,没必要在临时区折腾。

    • 默认不压缩:这是 LOH 最关键的特性!由于移动大块内存的成本极高,GC 在普通回收时默认不会压缩 LOH。这会导致 LOH 更容易产生内存碎片

      • 碎片化示例:假设 LOH 先后分配了对象 A、B、C,然后 B 不再使用被回收了。内存布局就变成了 [A][空闲][C]。这个“空闲”块可能不够大,无法放下一个新的大对象 D,即使总空闲空间足够,也会导致分配失败。
  3. 昂贵的回收:一旦需要回收 LOH(通常是在 Gen 2 回收时),或者当碎片化严重到一定程度时,GC 最终还是会进行压缩,但这将是一次非常耗时的操作,对程序性能影响显著。

比喻总结:LOH 就像是公共仓库里一个专门存放机床、汽车等大型设备的特殊区域。这些大件物品:

  • 搬运困难(分配和移动成本高)。

  • 直接归档(直接进 Gen 2)。

  • 很少移动(默认不压缩),但这导致这个区域的空间容易变得零碎不堪(碎片化),虽然总空间可能还很多,但可能找不到一块完整的空地放一个新的大设备。


总结与对比:

托管堆 - 代际回收过程
栈 Stack
值类型(int, struct等)
引用(指向堆的地址)
Gen 0 满时触发 GC0
幸存对象移至 Gen 1
分配在托管堆的
第 0 代(Gen 0)
Gen 1 满时触发 GC1
幸存对象移至 Gen 2
Gen 2 满时触发 Full GC
彻底清理所有代
new 关键字创建对象
对象大小判断
>= 85,000 字节?
分配在
大对象堆(LOH)
内存压缩
消除碎片
回收完成

流程图详解:

  1. 创建与分配

    • 使用 new 关键字创建对象时,CLR 首先会判断其大小。

    • 如果对象小于 85,000 字节,它会被分配在托管堆的第 0 代

    • 如果对象大于或等于 85,000 字节,它会被直接分配在大对象堆

  2. 代际回收与提升

    • 第 0 代:这是最常见、最频繁的分配区域。当 Gen 0 被填满时,会触发一次 Gen 0 GC。这次回收速度非常快,因为它只检查一小部分内存。在这次回收中幸存下来的对象,会被提升至第 1 代。

    • 第 1 代:作为 Gen 0 和 Gen 2 之间的缓冲区。当 Gen 1 也被填满时,会触发 Gen 1 GC。这次回收会清理 Gen 0 和 Gen 1。幸存下来的对象会被提升至第 2 代。

    • 第 2 代:存放长期存活的对象。当 Gen 2 被填满时,会触发一次 Full GC。这是一次全面的回收,会清理所有代(Gen 0, Gen 1, Gen 2)以及大对象堆。这是最耗时的回收操作,应尽量避免频繁发生。

  3. 大对象堆的特殊性:

    • LOH 中的对象不参与代际提升,它们从一开始就被视为第 2 代对象。

    • 因此,LOH 的回收只在 Full GC 发生时进行。

    • 由于移动大对象的成本很高,GC 在回收 LOH 时默认不进行压缩,这容易导致内存碎片。

  4. 栈的角色:

    • 如图所示,是一个完全独立的内存区域。

    • 它存储值类型(如局部变量 int i = 5;)和引用(如指针 MyClass obj;,即指向托管堆中实际对象地址的引用)。

    • 栈的管理是自动且高效的,随着方法调用结束而自动清理,完全不依赖 GC。

总结关系:

  • 快速、自动的临时储物区,存放原始数据和地址引用。

  • 托管堆主力对象仓库,采用代际模型来优化垃圾回收效率。绝大多数对象在此经历“从生到死”或“从新生代到老年代”的旅程。

  • 大对象堆是托管堆的特殊区域,享受“特权”(直接进入老年代)但也带来“麻烦”(回收成本高、易碎片化)。它的命运与 Full GC 紧密绑定。

如何编写对内存友好的代码?

  1. 值类型优先:对于小的、生命期短暂的数据,使用 struct(栈分配),减轻 GC 压力。

  2. 警惕装箱:避免将值类型赋值给 object 等引用类型,这会导致意外的堆分配(在 Gen 0)。

  3. 避免频繁的大对象分配:这是最重要的实践!不要频繁创建和丢弃大对象(如大数组),因为这会快速引发 Gen 2 GC 并导致 LOH 碎片化。解决方案是:

    • 对象池:使用 ArrayPool< T > 或 Microsoft.Extensions.ObjectPool 来重用大对象。

    • 缓存:对于真正需要单例的大对象,可以考虑缓存起来。

  4. 使用性能分析工具:如 dotnet-counters, dotnet-dump, Visual Studio Diagnostic Tool,来实时监控 GC 的各代回收频率和 LOH 大小,从而定位问题。

2.2 垃圾回收(GC)

内存管理(垃圾回收GC)

http://www.dtcms.com/a/390152.html

相关文章:

  • 【51单片机】【protues仿真】基于51单片机温度检测数码管系统
  • Sketch安装图文教程:从下载到账号注册完整流程
  • Day07_STM32 单片机 - 中断
  • 花瓶测试用例10条(基于质量模型)
  • C++ 之 【智能指针的简介】
  • Vue3 + xgplayer 实现多功能视频播放器:支持播放列表、自动连播与弹幕
  • 牛客算法基础noob46 约瑟夫环
  • TCP协议的详解
  • 【LeetCode】大厂面试算法真题回忆(136)——环中最长子串
  • Hystrix:熔断器
  • SQLark 实战 | 数据筛选与排序
  • 达梦Qt接口源码Qt6编译错误处理记录
  • 知识付费创作者:如何避免陷入跟风做内容的陷阱?
  • @once_differentiable 自定义算子的用处
  • 分子动力学--蛋白配体模拟
  • python第二节 基础语法及使用规范详解
  • 运维安全07 - JumpServer(堡垒机)介绍以及使用
  • 同一个电脑内两个进程间如何通信的几种方式
  • 《FastAPI零基础入门与进阶实战》第20篇:消息管理-封装
  • Pyside6 + QML - 信号与槽04 - Python 主动发射信号驱动 QML UI
  • 【系列文章】Linux系统中断的应用06-中断线程化
  • ruoyi-vue(十五)——布局设置,导航栏,侧边栏,顶部栏
  • 第13章 线程池配置
  • 任天堂获得新专利:和《宝可梦传说:阿尔宙斯》相关
  • Redis MONITOR 命令详解
  • 七、Java-多线程、网络编程
  • 三轴云台之动态补偿机制篇
  • MySQL备份与恢复实战指南:从原理到落地,守护数据安全
  • 手机上记录todolist待办清单的工具选择用哪一个?
  • 仓颉编程语言青少年基础教程:Interface(接口)