一文了解.Net的CLR、GC内存管理

  • 一文了解.Net的CLR、GC内存管理

    微软官方文档对内存管理和CLR的概述
    • 什么是托管代码?

      • 托管代码就是执行过程交由运行时管理的代码。 在这种情况下,相关的运行时称为公共语言运行时 (CLR),不管使用的是哪种实现(例如 Mono、.NET Framework 或 .NET Core/.NET 5+)。 CLR 负责提取托管代码、将其编译成机器代码,然后执行它。 除此之外,运行时还提供多个重要服务,例如自动内存管理、安全边界、类型安全,等等。
      • 托管代码是使用可在 .NET 上运行的一种高级语言(例如 C#、Visual Basic、F# 等)编写的。 使用相应的编译器编译以这些语言编写的代码时,无法获得机器代码, 而是获得 中间语言 代码,然后运行时会对其进行编译并将其执行。
    • 什么是中间语言?

      • 中间语言是编译使用高级 .NET 语言编写的代码后获得的结果。对使用其中一种语言编写的代码进行编译后,即可获得 IL 所生成的二进制代码。
      • 从高级代码生成 IL 后,你很有可能想要运行它。 CLR 此时将接管工作,启动 实时 (JIT) 编译过程,或者将代码从 IL 实时 编译成可以真正在 CPU 上运行的机器代码。 这样,CLR 就能确切地知道代码的作用,并可以有效地 管理 代码。
      • 中间语言有时也称为公共中间语言 (CIL) 或 Microsoft 中间语言 (MSIL)。
    • 自动内存管理

      自动内存管理是公共语言运行时在托管执行过程中提供的服务之一。 公共语言运行时的垃圾回收器为应用程序管理内存的分配和释放。
      • 分配内存

        • 初始化新进程时,运行时会为进程保留一个连续的地址空间区域。但是这个内存其实是虚拟的连续内存的,在物理内存上内存在物理上不一定连续的。这个保留的地址空间被称为托管堆。 托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。
      • 释放内存(GC如何表示对象需要回收、GC执行的流程?)

        • 垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间。 垃圾回收器在执行回收时,会释放应用程序不再使用的对象的内存。 它通过检查应用程序的根来确定不再使用的对象,垃圾回收器需要选定一系列的起点根(root)以保证对象的遍历,提供给垃圾回收器创建有向引用图的根,每次回收都会从根触发去遍历对象图中所有被引用的对象并标记,然后是清理和压缩未被引用的对象。
        • 为了改进性能,运行时为单独堆中的大型对象分配内存。 垃圾回收器会自动释放大型对象的内存。 但是,为了避免移动内存中的大型对象,不会压缩此内存。
    • 托管堆的级别和触发回收的时机

      为优化垃圾回收器的性能,将托管堆分为三代:第 0 代、第 1 代和第 2 代。运行时的垃圾回收器将新对象存储在第 0 级中。 在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第 1 级和第 2 级中。 GC的回收机制是通过检查扫描根引用和标记来进行确定回收的,这个根也称为GC根。执行第 1 代 GC 时,将同时回收第 1 代和第 0 代。 执行第 2 代 GC 时,将回收整个堆。 因此,第 2 代 GC 还可称为“完整 GC”
      • 第 0 代执行回收的时机

        垃圾回收器在第 0 级托管堆已满时执行回收。 如果应用程序在第 0 级托管堆已满时尝试新建对象,垃圾回收器将会发现第 0 级托管堆中没有可分配给该对象的剩余地址空间。 垃圾回收器执行回收,尝试为对象释放第 0 级托管堆中的地址空间。 垃圾回收器从检查第 0 级托管堆中的对象(而不是托管堆中的所有对象)开始执行回收。
      • 第 1 代对象的创建和执行回收时机

        垃圾回收器执行第 0 级托管堆的回收后,会压缩可访问对象的内存,垃圾回收器升级这些对象,并考虑第 1 代托管堆的这一部分对象。 因为未被回收的对象往往具有较长的生存期,所以将它们升级至更高的级别很有意义。 因此,垃圾回收器在每次执行第 0 代托管堆的回收时,不必重新检查第 1 代和第 2 代托管堆中的对象。
      • 第 2 代 对象的创建和执行回收时机

        圾回收器执行第 1 代托管堆的回收后,会压缩可访问对象的内存,垃圾回收器升级这些对象,并考虑第 2 代托管堆的这一部分对象。第 2 代托管堆中未被回收的对象会继续保留在第 2 代托管堆中,直到在将来的回收中确定它们无法访问为止。大型对象堆上的对象(有时称为 第 3 代)也在第 2 代中收集。
      • 垃圾回收器的优化引擎会决定是否需要检查较旧的级别中的对象

        如果第 0 级托管堆的回收没有回收足够的内存,不能使应用程序成功完成创建新对象的尝试,垃圾回收器就会先执行第 1 级托管堆的回收,然后再执行第 2 级托管堆的回收。 如果这样仍不能回收足够的内存,垃圾回收器将执行第 2、1 和 0 级托管堆的回收。 每次回收后,垃圾回收器都会压缩第 0 级托管堆中的可访问对象并将它们升级至第 1 级托管堆。 第 1 级托管堆中未被回收的对象将会升级至第 2 级托管堆。 由于垃圾回收器只支持三个级别,因此第 2 级托管堆中未被回收的对象会继续保留在第 2 级托管堆中,直到在将来的回收中确定它们为无法访问为止。
      • 非托管资源的内存释放

        对于应用程序创建的大多数对象,可以依赖垃圾回收器自动执行必要的内存管理任务。 但是,非托管资源需要显式清除。 最常用的非托管资源类型是包装操作系统资源的对象,例如,文件句柄、窗口句柄或网络连接。 虽然垃圾回收器可以跟踪封装非托管资源的托管对象的生存期,但却无法具体了解如何清理资源。 创建封装非托管资源的对象时,建议在公共 Dispose 方法中提供必要的代码以清理非托管资源。 通过提供 Dispose 方法,对象的用户可以在使用完对象后显式释放其内存。
    • 85kb的划分是指浅层对象还是深层对象?

      假如我们现在有一个对象Order对象如下定义,那么我们的order是应该在第0代还是在大对象堆呢?
      答案是在第0代,但是OrderItems是在大对象堆中的,所以85kb的只计算了浅层对象的大小,不计算对象的深层大小。
      public class Progarm{
        public static void main(){
          var order=new Order();// 0代
        }
      }
      public class Order {
        public OrderItem OrderItems{get private set;}=[5000];//>85kb字节 3代
      }
      public class OrderItem{
        public string ProductId{get; private set;}
      }
      
    • 垃圾回收

      • 基础

        • 优点

          • 开发人员不必手动释放内存。
          • 有效分配托管堆上的对象。
          • 回收不再使用的对象,清除它们的内存,并保留内存以用于将来分配。 托管对象会自动获取干净的内容来开始,因此,它们的构造函数不必对每个数据字段进行初始化。
          • 通过确保对象不能使用另一个对象的内容来提供内存安全。
        • 垃圾回收的条件

          • 系统具有低的物理内存。 这是通过 OS 的内存不足通知或主机指示的内存不足检测出来。
          • 由托管堆上已分配的对象使用的内存超出了可接受的阈值。 随着进程的运行,此阈值会不断地进行调整。
          • 调用GC.Collect方法。 几乎在所有情况下,你都不必调用此方法,因为垃圾回收器会持续运行。 此方法主要用于特殊情况和测试。
        • 垃圾回收过程中会有哪些操作?

          在垃圾回收启动之前,除了触发垃圾回收的线程,其它以外的所有托管线程都会被挂起。下图微软官方文档演示了触发垃圾回收并导致其他线程挂起的线程。
          • 标记阶段,找到并创建所有活动对象的列表。
          • 重定位阶段,用于更新对将要压缩的对象的引用。
          • 压缩阶段,用于回收由死对象占用的空间,并压缩幸存的对象。 压缩阶段将垃圾回收中幸存下来的对象移至段中时间较早的一端。因为第 2 代回收可以占用多个段,所以可以将已提升到第 2 代中的对象移动到时间较早的段中。 可以将第 1 代幸存者和第 2 代幸存者都移动到不同的段,因为它们已被提升到第 2 代。
          • 垃圾回收器使用以下信息来确定对象是否为活动对象:
            • 堆栈根。 由实时 (JIT) 编译器和堆栈查看器提供的堆栈变量。 JIT 优化可以延长或缩短报告给垃圾回收器的堆栈变量内的代码的区域。
            • 垃圾回收句柄。 指向托管对象且可由用户代码或公共语言运行时分配的句柄。
            • 静态数据。 应用程序域中可能引用其他对象的静态对象。 每个应用程序域都会跟踪其静态对象。
      • CLR中垃圾回收的分类

        从 .NET Framework 4.5 开始,后台垃圾回收可用于服务器 GC。 服务器 GC 是服务器垃圾回收的默认模式。后台工作区域垃圾回收使用一个专用的后台垃圾回收线程,而后台服务器垃圾回收使用多个线程。 通常一个逻辑处理器有一个专用线程。不同于工作站后台垃圾回收线程,这些后台服务器 GC 线程不会超时。

      • 工作站垃圾回收

        工作站垃圾回收 (GC) 是为客户端应用设计的。 它是独立应用的默认 GC 风格。对于托管应用(例如由 ASP.NET 托管的应用),由主机确定默认 GC 风格。工作站垃圾回收既可以是并发的,也可以是非并发的。 并发(或后台 )垃圾回收使托管线程能够在垃圾回收期间继续操作。后台垃圾回收替换 .NET Framework 4 及更高版本中的并行垃圾回收。 工作站垃圾回收使用用于只有一个处理器的计算机。

      • 服务器垃圾回收

        • 服务器垃圾回收主要用于需要高吞吐量和可伸缩行的服务器应用程序例如(WebApi)这种,在.Net Core和Framework4.5之后的版本中,服务器垃圾回收既可以是非并发也可以是后台执行的。
        • 服务垃圾回收会为每个CPU提供一个用于执行垃圾回收的一个堆和专用线程,并能同时回收这些堆。每个堆都包含一个小对象堆和一个大对象堆,并且所有的堆都可由用户代码访问。 不同堆上的对象可以相互引用。
        • 因为多个垃圾回收线程一起工作,所以对于相同大小的堆,服务器垃圾回收比工作站垃圾回收更快一些。
        • 服务器垃圾回收通常具有更大的段。 但是,这是通常情况:段大小特定于实现且可能更改。 调整应用程序时,不要假设垃圾回收器分配的段大小。
        • 服务器垃圾回收会占用大量资源。 例如,假设在一台有 4 个处理器的计算机上,运行着 12 个使用服务器 GC 的进程。 如果所有进程碰巧同时回收垃圾,它们会相互干扰,因为将在同一个处理器上调度 12 个线程。 如果进程处于活动状态,则最好不要让它们都使用服务器 GC。
      • 并行垃圾回收

        • 并发垃圾回收通过最大程度地减少因回收引起的暂停,使交互应用程序能够更快地响应。 在运行并发垃圾回收线程的大多数时间,托管线程可以继续运行。 此设计使得在发生垃圾回收时的暂停时间更短。
        • 并发垃圾回收在一个专用线程上执行。 默认情况下,CLR 将运行工作站垃圾回收,并在单处理器和多处理器计算机上同时启用并发垃圾回收。
      • 前台和后台的垃圾回收区别

        垃圾回收分为前台和后台所谓的后台回收是指 (gen2这一代所需要回收的对象还有大对象堆,大对象堆不会compact)那么前台回收说的就是gen0和gen1这一代所需要回收的对象了。

        • 前台垃圾回收

          发生前台垃圾回收的时候所有的托管线程会处于挂起阶段,也就是说这个时候是GC线程执行的阶段,其他线程不处理任务。另外就是前台的垃圾回收也是由后台垃圾回收线程去执行的,它是这么做的如果后台垃圾回收的线程在检测是否有前台发起垃圾回收指令,如果收到了就挂起后台的垃圾回收的线程,执行前台的垃圾回收的线程。在前台垃圾回收完成之后,专用的后台垃圾回收线程和用户线程将继续。
        • 后台垃圾回收

          后台垃圾回收可以消除并发垃圾回收所带来的分配限制,因为在后台垃圾回收期间,可发生暂时垃圾回收。 后台垃圾回收可以删除暂存世代中的死对象。如果需要,它还可以在第1代垃圾回收期间扩展堆。
    • 大对象堆(LOH)和小对象堆分别是什么?

      加载 CLR 时,GC 分配两个初始堆段:一个用于小型对象(小型对象堆或 SOH),一个用于大型对象(大型对象堆)。通过将托管对象置于这些托管堆段上来满足分配请求。 如果该对象小于 85kb,则将它置于 SOH 的段上,否则,将它置于 LOH 段。 触发垃圾回收后,GC 将寻找存在的对象并将它们压缩。 但是由于压缩费用很高,GC 会扫过 LOH,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。 相邻的被清除对象将组成一个自由对象。用户代码只能在第 0 代(小型对象)或 LOH(大型对象)中分配。只有 GC 可以在第 1 代(通过提升第 0 代回收未处理的对象)和第 2 代(通过提升第 1 代和第 2 代回收未处理的对象)中“分配”对象。
      • .Net GC将需要回收的对象分为了大对象堆和小对象堆,如果是大对象的话那么它的某些特性比小对象显得更为重要,例如复制一个对象到内存堆的其他位置的性能消耗会相当高,因此GC 会直接将大对象放置到大对象堆上面,大对象和小对象的区分默认是以85kb来作为区分,这个数字是可改的。
      • 小对象一般是指第0代和第1代中的对象,这类对象在GC 回收的时候执行效率会很高,所以为了优化GC 回收器的性能所以才会区分代数,大多数对象都会通过第 0 代GC 回收进行回收,所以不会保留到下一代。大对象堆的回收时机是随着2代GC 回收而执行,2代GC也是完整GC。
      • 大对象堆在什么情况会启动垃圾回收?

        • 分配超出第0代或者大对象阀值。
        • 调用GC.Collect方法。
        • 系统处于内存不足的状况下同样也会回收。
      • 为什么会存在大对象堆?

        • 分配成本。
        • 回收成本。
        • 具有引用类型的数组元素。
    • GC / IDisposable / 析构函数三者的关系?

      • GC是负责管理托管资源的内存的,它负责不存在引用地址对象的内存。
      • 析构函数和IDisposable的接口的区别:析构是没有执行顺序的,析构函数的执行时机无法确定,所以官方给提供了IDisposable的接口,让开发人员可以在代码中显示的调用Dispose释放方法。
    • 内存溢出如何发现和解决?

posted @ 2022-08-26 10:48  九两白菜粥  阅读(160)  评论(0编辑  收藏  举报