C#基础_垃圾回收(未完)

2021/12/31 12:07:45

本文主要是介绍C#基础_垃圾回收(未完),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1.关于垃圾回收

.NET 的垃圾回收器管理应用程序的内存分配和释放。 每当有对象新建时,公共语言运行时都会从托管堆为对象分配内存。 只要托管堆中有地址空间,运行时就会继续为新对象分配空间。 不过,内存并不是无限的。 垃圾回收器最终必须执行垃圾回收来释放一些内存。 垃圾回收器的优化引擎会根据所执行的分配来确定执行回收的最佳时机。 执行回收时,垃圾回收器会在托管堆中检查应用程序不再使用的对象,然后执行必要的操作来回收其内存。

垃圾回收:在公共语言运行时 (CLR) 中,垃圾回收器 (GC) 用作自动内存管理器。 垃圾回收器管理应用程序的内存分配和释放。 对于使用托管代码的开发人员而言,这就意味着不必编写执行内存管理任务的代码。 自动内存管理可解决常见问题,例如,忘记释放对象并导致内存泄漏,或尝试访问已释放对象的内存。

1.1 优点

垃圾回收器具有以下优点:

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

1.2 内存的基础知识


C#.Net/CoreCLR性能调优,基准测试:https://www.ixigua.com/6951281664077234719

下面的列表总结了重要的 CLR 内存概念。

  • 每个进程都有其自己单独的虚拟地址空间。 同一台计算机上的所有进程共享相同的物理内存和页文件(如果有)。

  • 默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式虚拟地址空间。

  • 作为一名应用程序开发人员,你只能使用虚拟地址空间,请勿直接操控物理内存。 垃圾回收器为你分配和释放托管堆上的虚拟内存。

    如果你编写的是本机代码,请使用 Windows 函数处理虚拟地址空间。 这些函数为你分配和释放本机堆上的虚拟内存。

  • 虚拟内存有三种状态:

    状态 描述
    Free 该内存块没有引用关系,可用于分配。
    保留 内存块可供你使用,并且不能用于任何其他分配请求。 但是,在该内存块提交之前,你无法将数据存储到其中。
    已提交 内存块已指派给物理存储。
  • 可能会存在虚拟地址空间碎片。 就是说地址空间中存在一些被称为孔的可用块。 当请求虚拟内存分配时,虚拟内存管理器必须找到满足该分配请求的足够大的单个可用块。 即使有 2 GB 可用空间,2 GB 分配请求也会失败,除非所有这些可用空间都位于一个地址块中。

  • 如果没有足够的可供保留的虚拟地址空间或可供提交的物理空间,则可能会用尽内存。

    即使在物理内存压力(即物理内存的需求)较低的情况下也会使用页文件。 首次出现物理内存压力较高的情况时,操作系统必须在物理内存中腾出空间来存储数据,并将物理内存中的部分数据备份到页文件中。 该数据只会在需要时进行分页,所以在物理内存压力较低的情况下也可能会进行分页。

image-20211228133251997

内存分配: 初始化新进程时,运行时会为进程保留一个连续的地址空间区域。 这个保留的地址空间被称为托管堆。 托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。 最初,该指针设置为指向托管堆的基址。 托管堆上部署了所有引用类型。 应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存。 应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。 只要地址空间可用,垃圾回收器就会继续以这种方式为新对象分配空间。

从托管堆中分配内存要比非托管内存分配速度快。 由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。 另外,由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。

内存释放:

垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间。 垃圾回收器在执行回收时,会释放应用程序不再使用的对象的内存。 它通过检查应用程序的根来确定不再使用的对象。 应用程序的根包含线程堆栈上的静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列。 每个根或者引用托管堆中的对象,或者设置为空。 垃圾回收器可以为这些根请求其余运行时。 垃圾回收器使用此列表创建一个图表,其中包含所有可从这些根中访问的对象

不在该图表中的对象将无法从应用程序的根中访问。 垃圾回收器会考虑无法访问的对象垃圾,并释放为它们分配的内存。 在回收中,垃圾回收器检查托管堆,查找无法访问对象所占据的地址空间块。 发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。 在压缩了可访问对象的内存后,垃圾回收器就会做出必要的指针更正,以便应用程序的根指向新地址中的对象。 它还将托管堆指针定位至最后一个可访问对象之后。

只有在回收发现大量的无法访问的对象时,才会压缩内存。 如果托管堆中的所有对象均未被回收,则不需要压缩内存。

为了改进性能,运行时为单独堆中的大型对象分配内存。 垃圾回收器会自动释放大型对象的内存。 但是,为了避免移动内存中的大型对象,通常不会压缩此内存。

主要分为二个步骤:

标记对象和压缩托管堆

参考资料:https://www.cnblogs.com/wilber2013/p/4357910.html

1.3 垃圾回收的条件


当满足以下条件之一时将发生垃圾回收:

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

1.4 托管堆


在垃圾回收器由 CLR 初始化之后,它会分配一段内存用于存储和管理对象。 此内存称为托管堆(与操作系统中的本机堆相对)。

每个托管进程都有一个托管堆。 进程中的所有线程都在同一堆上为对象分配内存。

若要保留内存,垃圾回收器会调用 Windows VirtualAlloc 函数,并且每次为托管应用保留一个内存段。 垃圾回收器还会根据需要保留内存段,并调用 Windows VirtualFree 函数,将内存段释放回操作系统(在清除所有对象的内存段后)。

堆上分配的对象越少,垃圾回收器必须执行的工作就越少。 分配对象时,请勿使用超出你需求的舍入值,例如在仅需要 15 个字节的情况下分配了 32 个字节的数组。

当触发垃圾回收时,垃圾回收器将回收由非活动对象占用的内存。 回收进程会对活动对象进行压缩,以便将它们一起移动,并移除死空间,从而使堆更小一些。 这将确保一起分配的对象全都位于托管堆上,从而保留它们的局部性。

垃圾回收的侵入性(频率和持续时间)是由分配的数量和托管堆上保留的内存数量决定的。

此堆可视为两个堆的累计:大对象堆和小对象堆。 大对象堆包含大小不少于 85,000 个字节的对象,这些对象通常是数组。 非常大的实例对象是很少见的。

1.5 代数


GC 算法基于几个注意事项:

  • 压缩托管堆的一部分内存要比压缩整个托管堆速度快。
  • 较新的对象生存期较短,而较旧的对象生存期则较长。
  • 较新的对象趋向于相互关联,并且大致同时由应用程序访问。

垃圾回收主要在回收短生存期对象时发生。 为优化垃圾回收器的性能,将托管堆分为三代:第 0 代、第 1 代和第 2 代,因此它可以单独处理长生存期和短生存期对象。 垃圾回收器将新对象存储在第 0 代中。 在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第 1 级和第 2 级中。 因为压缩托管堆的一部分要比压缩整个托管堆速度快,所以此方案允许垃圾回收器在每次执行回收时释放特定级别的内存,而不是整个托管堆的内存。

  • 第 0 代。 这是最年轻的代,其中包含短生存期对象。 短生存期对象的一个示例是临时变量。 垃圾回收最常发生在此代中。

    新分配的对象构成新一代对象,并隐式地成为第 0 代集合。 但是,如果它们是大型对象,它们将延续到大型对象堆 (LOH),这有时称为第 3 代。 第 3 代是在第 2 代中逻辑收集的物理生成。

    大多数对象通过第 0 代中的垃圾回收进行回收,不会保留到下一代。

    如果应用程序在第 0 代托管堆已满时尝试创建新对象,垃圾回收器将执行收集,以尝试为该对象释放地址空间。 垃圾回收器从检查第 0 级托管堆中的对象(而不是托管堆中的所有对象)开始执行回收。 单独回收第 0 代托管堆通常可以回收足够的内存,这样,应用程序便可以继续创建新对象。

  • 第 1 代。 这一代包含短生存期对象并用作短生存期对象和长生存期对象之间的缓冲区。

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

    如果第 0 代托管堆的回收没有回收足够的内存供应用程序创建新对象,垃圾回收器就会先执行第 1 代托管堆的回收,然后再执行第 2 代托管堆的回收。 第 1 级托管堆中未被回收的对象将会升级至第 2 级托管堆。

  • 第 2 代。 这一代包含长生存期对象。 长生存期对象的一个示例是服务器应用程序中的一个包含在进程期间处于活动状态的静态数据的对象。

    第 2 代托管堆中未被回收的对象会继续保留在第 2 代托管堆中,直到在将来的回收中确定它们无法访问为止。

    大型对象堆上的对象(有时称为 第 3 代)也在第 2 代中收集。

当条件得到满足时,垃圾回收将在特定代上发生。 回收某个代意味着回收此代中的对象及其所有更年轻的代。 第 2 代垃圾回收也称为完整垃圾回收,因为它回收所有代中的对象(即,托管堆中的所有对象)。

幸存和提升

垃圾回收中未回收的对象也称为幸存者,并会被提升到下一代:

  • 第 0 代垃圾回收中未被回收的对象将会升级至第 1 代。
  • 第 1 代垃圾回收中未被回收的对象将会升级至第 2 代。
  • 第 2 代垃圾回收中未被回收的对象将仍保留在第 2 代。

当垃圾回收器检测到某个代中的幸存率很高时,它会增加该代的分配阈值。 下次回收将回收非常大的内存。 CLR 持续在以下两个优先级之间进行平衡:不允许通过延迟垃圾回收,让应用程序的工作集获取太大内存,以及不允许垃圾回收过于频繁地运行。

暂时代和暂时段

因为第 0 代和第 1 代中的对象的生存期较短,因此,这些代被称为“暂时代”。

暂时代在称为“暂时段”的内存段中进行分配。 垃圾回收器获取的每个新段将成为新的暂时段,并包含在第 0 代垃圾回收中幸存的对象。 旧的暂时段将成为新的第 2 代段。

根据系统为 32 位还是 64 位以及它正在哪种类型的垃圾回收器(工作站或服务器 GC)上运行,暂时段的大小发生相应变化。 下表显示了暂时段的默认大小。

工作站/服务器 GC 32 位 64 位
工作站 GC 16 MB 256 MB
服务器 GC 64 MB 4 GB
服务器 GC(具有 4 个以上的逻辑 CPU) 32 MB 2 GB
服务器 GC(具有 8 个以上的逻辑 CPU) 16 MB 1 GB

暂时段可以包含第 2 代对象。 第 2 代对象可使用多个段(在内存允许的情况下进程所需的任意数量)。

从暂时垃圾回收中释放的内存量限制为暂时段的大小。 释放的内存量与死对象占用的空间成比例。

1.6 垃圾回收过程中发生的情况


垃圾回收分为以下几个阶段:

  • 标记阶段,找到并创建所有活动对象的列表。

  • 重定位阶段,用于更新对将要压缩的对象的引用。

  • 压缩阶段,用于回收由死对象占用的空间,并压缩幸存的对象。 压缩阶段将垃圾回收中幸存下来的对象移至段中时间较早的一端。

    因为第 2 代回收可以占用多个段,所以可以将已提升到第 2 代中的对象移动到时间较早的段中。 可以将第 1 代幸存者和第 2 代幸存者都移动到不同的段,因为它们已被提升到第 2 代。

    通常,由于复制大型对象会造成性能代偿,因此不会压缩大型对象堆 (LOH)。 但是,在 .NET Core 和 .NET Framework 4.5.1 及更高版本中,可以根据需要使用 GCSettings.LargeObjectHeapCompactionMode 属性按需压缩大型对象堆。 此外,当通过指定以下任一项设置硬限制时,将自动压缩 LOH:

    • 针对容器的内存限制。
    • GCHeapHardLimit 或 GCHeapHardLimitPercent 运行时配置选项。

垃圾回收器使用以下信息来确定对象是否为活动对象:

  • 堆栈根。 由实时 (JIT) 编译器和堆栈查看器提供的堆栈变量。 JIT 优化可以延长或缩短报告给垃圾回收器的堆栈变量内的代码的区域。
  • 垃圾回收句柄。 指向托管对象且可由用户代码或公共语言运行时分配的句柄。
  • 静态数据。 应用程序域中可能引用其他对象的静态对象。 每个应用程序域都会跟踪其静态对象。

在垃圾回收启动之前,除了触发垃圾回收的线程以外的所有托管线程均会挂起。

下图演示了触发垃圾回收并导致其他线程挂起的线程。

线程触发垃圾回收时

1.7 非托管资源


对于应用程序创建的大多数对象,可以依赖垃圾回收自动执行必要的内存管理任务。 但是,非托管资源需要显式清除。 最常用的非托管资源类型是包装操作系统资源的对象,例如,文件句柄、窗口句柄或网络连接。 虽然垃圾回收器可以跟踪封装非托管资源的托管对象的生存期,但却无法具体了解如何清理资源。

创建封装非托管资源的对象时,建议在公共 Dispose 方法中提供必要的代码以清理非托管资源。 通过提供 Dispose 方法,对象的用户可以在使用完对象后显式释放其内存。 使用封装非托管资源的对象时,务必要在需要时调用 Dispose

还必须提供一种释放非托管资源的方法,以防类型使用者忘记调用 Dispose。 可以使用安全句柄来包装非托管资源,也可以重写 Object.Finalize() 方法。

2. 工作站和服务器垃圾回收

垃圾回收器可自行优化并且适用于多种方案。 不过,你可以基于工作负载的特征设置垃圾回收的类型。 CLR 提供了以下类型的垃圾回收:

  • 工作站垃圾回收 (GC) 是为客户端应用设计的。 它是独立应用的默认 GC 风格。 对于托管应用(例如由 ASP.NET 托管的应用),由主机确定默认 GC 风格。

    工作站垃圾回收既可以是并发的,也可以是非并发的。 并发(或后台 )垃圾回收使托管线程能够在垃圾回收期间继续操作。 后台垃圾回收替换 .NET Framework 4 及更高版本中的并行垃圾回收。

  • 服务器垃圾回收,用于需要高吞吐量和可伸缩性的服务器应用程序。

    • 在 .NET Core 中,服务器垃圾回收既可以是非并发也可以是后台执行。
    • 在 .NET Framework 4.5 和更高版本中,服务器垃圾回收既可以是非并发也可以是后台执行。 在 .NET Framework 4 和以前的版本中,服务器垃圾回收非并行运行。

下图演示了服务器上执行垃圾回收的专用线程:

服务器垃圾回收线程

性能注意事项

工作站 GC

以下是工作站垃圾回收的线程处理和性能注意事项:

  • 回收发生在触发垃圾回收的用户线程上,并保留相同优先级。 因为用户线程通常以普通优先级运行,所以垃圾回收器(在普通优先级线程上运行)必须与其他线程竞争 CPU 时间。 (运行本机代码的线程不会由于服务器或工作站垃圾回收而挂起。)

  • 工作站垃圾回收始终用于只有一个处理器的计算机,无论配置设置如何。

服务器 GC

​ 以下是服务器垃圾回收的线程处理和性能注意事项:

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

3. 后台垃圾回收

在后台垃圾回收 (GC) 中,在进行第 2 代回收的过程中,将会根据需要收集暂时代(第 0 代和第 1 代)。 后台垃圾回收是在一个或多个专用线程上执行的,具体取决于它是后台还是服务器 GC,它只适用于第 2 代回收。

默认启用后台垃圾回收。 可以在 .NET Framework 应用中使用 gcConcurrent 配置设置或 .NET Core 和 .NET 5 及更高版本应用中的 System.GC.Concurrent 来启用或禁用后台垃圾回收。

备注
后台垃圾回收替换在 .NET Framework 4 及更高版本中可用的[并行垃圾回收]。 在 .NET Framework 4 中,仅支持工作站垃圾回收。 从 .NET Framework 4.5 开始,后台垃圾回收可用于工作站和服务器垃圾回收 。

后台垃圾回收期间对暂时代的回收称为“前台”垃圾回收。 发生前台垃圾回收时,所有托管线程都将被挂起。

当后台垃圾回收正在进行并且你已在第 0 代中分配了足够的对象时,CLR 将执行第 0 代或第 1 代前台垃圾回收。 专用的后台垃圾回收线程将在常见的安全点上进行检查以确定是否存在对前台垃圾回收的请求。 如果存在,则后台回收将挂起自身以便前台垃圾回收可以发生。 在前台垃圾回收完成之后,专用的后台垃圾回收线程和用户线程将继续。

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

后台工作站与服务器 GC

从 .NET Framework 4.5 开始,后台垃圾回收可用于服务器 GC。 服务器 GC 是服务器垃圾回收的默认模式。

后台服务器垃圾回收与后台工作站垃圾回收具有类似功能,但有一些不同之处:

  • 后台工作区域垃圾回收使用一个专用的后台垃圾回收线程,而后台服务器垃圾回收使用多个线程。 通常一个逻辑处理器有一个专用线程。
  • 不同于工作站后台垃圾回收线程,这些后台服务器 GC 线程不会超时。

下图显示对独立专用线程执行的后台工作站垃圾回收:

后台工作站垃圾回收

下图显示对独立专用线程执行的后台服务器垃圾回收:

后台服务器垃圾回收

并行垃圾回收

在.net更高的版本中,后台垃圾回收取代了并行垃圾回收。

4.大型对象堆

4.1Windows 系统上的大型对象堆

.NET 垃圾回收器 (GC) 将对象分为小型大型对象。 如果是大型对象,它的某些特性将比对象较小时显得更为重要。 例如,压缩大型对象—(也就是在内存中将其复制到堆上的其他位置)—的费用相当高。 因此,垃圾回收器将大型对象放置在大型对象堆 (LOH) 上。 本文将讨论符合什么条件的对象才能称之为大型对象,如何回收大型对象,以及大型对象具备哪些性能意义。

LOH:大型对象堆

SOH:小型对象堆

对象如何在LOH上结束

如果对象的大小大于或等于 85,000 字节,将被视为大型对象。

垃圾回收器是分代回收器。 它包含三代:第 0 代、第 1 代和第 2 代。 包含 3 代的原因是,在优化良好的应用中,大部分对象都在第 0 代就清除了。 例如,在服务器应用中,与每个请求相关的分配应在请求完成后清除。 仍存在的分配请求将转到第 1 代,并在那里进行清除。 从本质上讲,第 1 代是新对象区域与生存期较长的对象区域之间的缓冲区。

小型对象始终在第 0 代中进行分配,或者根据它们的生存期,可能会提升为第 1 代或第 2 代。 大型对象始终在第 2 代中进行分配。

大型对象属于第 2 代,因为只有在第 2 代回收期间才能回收它们。 回收一代时,同时也会回收它前面的所有代。 例如,执行第 1 代 GC 时,将同时回收第 1 代和第 0 代。 执行第 2 代 GC 时,将回收整个堆。 因此,第 2 代 GC 还可称为“完整 GC”。 本文引用第 2 代 GC 而不是完整 GC,但这两个术语是可以互换的。

代可提供 GC 堆的逻辑视图。 实际上,对象存在于托管堆段中。 托管堆段是 GC 通过调用 VirtualAlloc 功能代表托管代码在操作系统上保留的内存块。 加载 CLR 时,GC 分配两个初始堆段:一个用于小型对象(小型对象堆或 SOH)一个用于大型对象(大型对象堆)

然后,通过将托管对象置于这些托管堆段上来满足分配请求。 如果该对象小于 85,000 字节,则将它置于 SOH 的段上,否则,将它置于 LOH 段。 随着分配到各段上的对象越来越多,会以较小块的形式提交这些段。 对于 SOH,GC 未处理的对象将提升为下一代。 第 0 代回收未处理的对象现在视为第 1 代对象,以此类推。 但是,最后一代回收未处理的对象仍会被视为最后一代中的对象。 也就是说,第 2 代垃圾回收未处理的对象仍是第 2 代对象;LOH 未处理的对象仍是 LOH 对象(由第 2 代回收)。

用户代码只能在第 0 代(小型对象)或 LOH(大型对象)中分配。 只有 GC 可以在第 1 代(通过提升第 0 代回收未处理的对象)和第 2 代(通过提升第 1 代和第 2 代回收未处理的对象)中“分配”对象。

触发垃圾回收后,GC 将寻找存在的对象并将它们压缩。 但是由于压缩费用很高,GC 会扫过 LOH,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。 相邻的被清除对象将组成一个自由对象。

.NET Core 和 .NET Framework(从 .NET Framework 4.5.1 开始)包括 GCSettings.LargeObjectHeapCompactionMode 属性,该属性可让用户指定在下一完整阻止 GC 期间压缩 LOH。 并且在以后,.NET 可能会自动决定压缩 LOH。 这就意味着,如果分配了大型对象并希望确保它们不被移动,则应将其固定起来。

图 1 说明了一种情况,在第一次第 0 代 GC 后 GC 形成了第 1 代,其中 Obj1Obj3 被清除;在第一次第 1 代 GC 后形成了第 2 代,其中 Obj2Obj5 被清除。 请注意此图和下图仅用于说明,它们只包含能更好展示堆上的情况的极少几个对象。 实际上,GC 中通常包含更多的对象。

图 1:第 0 代 GC 和第 1 代 GC

图 1:第 0 代和第 1 代 GC。

图 2 显示了第 2 代 GC 发现 Obj1Obj2 被清除后,GC 在内存中形成了相邻的可用空间,由 Obj1Obj2 占用,然后用于满足 Obj4 的分配要求。 从最后一个对象 Obj3 到此段末尾的空间仍可用于满足分配请求。

图 2:第 2 代 GC 后
图 2:第 2 代 GC 后

如果没有足够的可用空间来容纳大型对象分配请求,GC 首先尝试从操作系统获取更多段。 如果失败了,它将触发第 2 代 GC,试图释放部分空间。

在第 1 代或第 2 代 GC 期间,垃圾回收器会通过调用 VirtualFree 功能将不包含活动对象的段释放回操作系统。 将退回最后一个活动对象到段末尾的空间(第 0 代/第 1 代存在的短暂段上的空间除外,垃圾回收器会在该段上会保存部分提交内容,因为应用程序将在其中立即分配)。 而且,尽管已重置可用空间,但仍会提交它们,这意味着操作系统无需将其中的数据重新写入磁盘。

由于 LOH 仅在第 2 代 GC 期间进行回收,所以 LOH 段仅在此类 GC 期间可用。 图 3 说明了一种情况,在此情况下,垃圾回收器将某段(段 2)释放回操作系统并且退回剩余段上更多的空间。 如果需要使用该段末尾的已退回空间来满足大型对象分配请求,它会再次提交该内存。 (有关提交/退回的解释说明,请参阅 VirtualAlloc 的文档)。

图 3:第 2 代 GC 后的 LOH
图 3:第 2 代 GC 后的 LOH

4.2何时收集大型对象

通常情况下,出现以下三种情形中的任一情况,都会执行 GC:

  • 分配超出第 0 代或大型对象阈值。

    阈值是某代的属性。 垃圾回收器在其中分配对象时,会为代设置阈值。 超出阈值后,会在该代上触发 GC。 因此,分配小型或大型对象时,需要分别使用第 0 代和 LOH 的阈值。 当垃圾回收器分配到第 1 代和第 2 代中时,将使用它们的阈值。 运行此程序时,会动态调整这些阈值。

    这是典型情况,大部分 GC 执行都因为托管堆上的分配。

  • 调用 GC.Collect 方法。

    如果调用无参数 GC.Collect() 方法,或另一个重载作为参数传递到 GC.MaxGeneration,将会一起收集 LOH 和剩余的托管堆。

  • 系统处于内存不足的状况。

    垃圾回收器收到来自操作系统 的高内存通知时,会发生以上情况。 如果垃圾回收器认为执行第 2 代 GC 会有效率,它将触发第 2 代。

4.3LOH性能意义

大型对象堆上的分配通过以下几种方式影响性能。

  • 分配成本。

    CLR 确保清除了它提供的每个新对象的内存。 这意味着大型对象的分配成本完全由清理的内存(除非触发了 GC)决定。 如果需要 2 轮才能清除一个字节,即需要 170,000 轮才能清除最小的大型对象。 清除 2GHz 计算机上 16MB 对象的内存大约需要 16ms。 这些成本相当大。

  • 回收成本。

    因为 LOH 和第 2 代一起回收,如果超出了它们之中任何一个的阈值,则触发第 2 代回收。 如果由于 LOH 触发第 2 代回收,第 2 代没有必要在 GC 后变得更小。 如果第 2 代上数据不多,则影响较小。 但是,如果第 2 代很大,则触发多次第 2 代 GC 可能会产生性能问题。 如果很多大型对象都在非常短暂的基础上进行分配,并且拥有大型 SOH,则可能会花费太多时间来执行 GC。 除此之外,如果连续分配并且释放真正的大型对象,那么分配成本可能会增加。

  • 具有引用类型的数组元素。

    LOH 上的特大型对象通常是数组(很少会有非常大的实例对象)。 如果数组的元素有丰富的引用,则可能产生成本;如果元素没有丰富的引用,将不会产生此类成本。 如果元素不包含任何引用,则垃圾回收器根本无需处理此数组。 例如,如果使用数组存储二进制树中的节点,一种实现方法是按实际节点引用某个节点的左侧节点和右侧节点:

    C#复制

    class Node
    {
       Data d;
       Node left;
       Node right;
    };
    
    Node[] binary_tr = new Node [num_nodes];
    

    如果 num_nodes 非常大,则垃圾回收器需要处理每个元素的至少两个引用。 另一种方法是存储左侧节点和右侧节点的索引:

    C#复制

    class Node
    {
       Data d;
       uint left_index;
       uint right_index;
    } ;
    

    不要将左侧节点的数据引用为 left.d,而是将其引用为 binary_tr[left_index].d。 而垃圾回收器无需查看左侧节点和右侧节点的任何引用。

在这三种因素中,前两个通常比第三个更重要。 因此,建议分配重复使用的大型对象池,而不是分配临时大型对象。

4.4 收集 LOH 的性能数据

收集特定区域的性能数据之前,应完成以下操作:

  1. 找到应查看此区域的证据。
  2. 排查你知道的其他区域,确保未发现可解释上述性能问题的内容。

参阅博客尝试找出解决方案之前先了解问题获取内存和 CPU 的基础知识的详细信息。

可使用以下工具来收集 LOH 性能数据:

  • .NET CLR 内存性能计数器
  • ETW 事件
  • 调试器

.NET CLR 内存性能计数器

这些性能计数器通常是调查性能问题的第一步(但是推荐使用 ETW 事件)。 通过添加所需计数器配置性能监视器,如图 4 所示。 与 LOH 相关的是:

  • 第 2 代回收次数

    显示自进程开始起第 2 代 GC 发生的次数。 此计数器在第 2 代回收结束时递增(也称为完整垃圾回收)。 此计数器显示上次观测的值。

  • 大型对象堆大小

    以字节显示当前大小,包括 LOH 的可用空间。 此计数器在垃圾回收结束时更新,不在每次分配时更新。

查看性能计数器的常用方法是使用性能监视器 (perfmon.exe)。 使用“添加计数器”可为关注的进程添加感兴趣的计数器。 可将性能计数器数据保存在日志文件中,如图 4 所示:

屏幕截图显示了如何添加性能计数器。 图 4:第 2 代 GC 后的 LOH

也可以编程方式查询性能计数器。 大部分人在例行测试过程中都采用此方式进行收集。 如果发现计数器显示的值不正常,则可以使用其他方法获得更多详细信息以帮助调查。

ETW 事件

垃圾回收器提供丰富的 ETW 事件集,帮助了解堆的工作内容和工作原理。 以下博客文章演示了如何使用 ETW 收集和了解 GC 事件:

  • GC ETW 事件 - 1
  • GC ETW 事件 - 2
  • GC ETW 事件 - 3
  • GC ETW 事件 - 4

若要标识由临时 LOH 分配造成的过多第 2 代 GC 次数,请查看 GC 的“触发原因”列。 有关仅分配临时大型对象的简单测试,可使用以下 PerfView 命令行收集 ETW 事件的信息:

控制台复制

perfview /GCCollectOnly /AcceptEULA /nogui collect

结果类似于以下类容:

屏幕截图显示了 PerfView 中的 ETW 事件。 图 5:使用 PerfView 显示的 ETW 事件

如下所示,所有 GC 都是第 2 代 GC,并且都由 AllocLarge 触发,这表示分配大型对象会触发此 GC。 我们知道这些分配是临时的,因为“LOH 未清理率 %”列显示为 1%。

可以收集显示分配这些大写对象的人员的其他 ETW 事件。 以下命令行:

控制台复制

perfview /GCOnly /AcceptEULA /nogui collect

收集 AllocationTick 事件,大约每 10 万次分配就会触发该事件。 换句话说,每次分配大型对象都会触发事件。 然后可查看某个 GC 堆分配视图,该视图显示分配大型对象的调用堆栈:

屏幕截图显示了垃圾回收器堆视图。 图 6:GC 堆分配视图

如图所示,这是从 Main 方法分配大型对象的简单测试。

调试器

如果只有内存转储,则需要查看 LOH 上实际有哪些对象,你可使用 .NET 提供的 SoS 调试器扩展来查看。

备注

此部分提到的调试命令适用于 Windows 调试器。

以下内容显示了分析 LOH 的示例输出:

控制台复制

0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment   begin allocated     size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment   begin allocated     size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT   Count   TotalSize Class Name
001521d0       66     2081792     Free
7912273c       63     6663696 System.Byte[]
7912254c       4     8008736 System.Object[]
Total 133 objects

LOH 堆大小为 (16,754,224 + 16,699,288 + 16,284,504) = 49,738,016 字节。 在地址 023e1000 和地址 033db630 之间,8,008,736 字节由 System.Object 对象的数组占用,6,663,696 字节由 System.Byte 对象的数组占用,2,081,792 字节由可用空间占用。

有时,调试器显示 LOH 的总大小少于 85,000 个字节。 这是由于运行时本身使用 LOH 分配某些小于大型对象的对象引起的。

因为不会压缩 LOH,有时会怀疑 LOH 是碎片源。 碎片表示:

  • 托管堆的碎片由托管对象之间的可用空间量来表示。 在 SoS 中,!dumpheap –type Free 命令显示托管对象之间的可用空间量。

  • 虚拟内存 (VM) 地址空间的碎片是标识为 MEM_FREE 的内存。 可在 windbg 中使用各种调试器命令来获取碎片。

    以下示例显示 VM 空间中的碎片:

    控制台复制

    0:000> !address
    00000000 : 00000000 - 00010000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    00010000 : 00010000 - 00002000
    Type     00020000 MEM_PRIVATE
    Protect 00000004 PAGE_READWRITE
    State   00001000 MEM_COMMIT
    Usage   RegionUsageEnvironmentBlock
    00012000 : 00012000 - 0000e000
    Type     00000000
    Protect 00000001 PAGE_NOACCESS
    State   00010000 MEM_FREE
    Usage   RegionUsageFree
    … [omitted]
    -------------------- Usage SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Pct(Busy)   Usage
    701000 (   7172) : 00.34%   20.69%   : RegionUsageIsVAD
    7de15000 ( 2062420) : 98.35%   00.00%   : RegionUsageFree
    1452000 (   20808) : 00.99%   60.02%   : RegionUsageImage
    300000 (   3072) : 00.15%   08.86%   : RegionUsageStack
    3000 (     12) : 00.00%   00.03%   : RegionUsageTeb
    381000 (   3588) : 00.17%   10.35%   : RegionUsageHeap
    0 (       0) : 00.00%   00.00%   : RegionUsagePageHeap
    1000 (       4) : 00.00%   00.01%   : RegionUsagePeb
    1000 (       4) : 00.00%   00.01%   : RegionUsageProcessParametrs
    2000 (       8) : 00.00%   00.02%   : RegionUsageEnvironmentBlock
    Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)
    
    -------------------- Type SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    7de15000 ( 2062420) : 98.35%   : <free>
    1452000 (   20808) : 00.99%   : MEM_IMAGE
    69f000 (   6780) : 00.32%   : MEM_MAPPED
    6ea000 (   7080) : 00.34%   : MEM_PRIVATE
    
    -------------------- State SUMMARY --------------------------
    TotSize (     KB)   Pct(Tots) Usage
    1a58000 (   26976) : 01.29%   : MEM_COMMIT
    7de15000 ( 2062420) : 98.35%   : MEM_FREE
    783000 (   7692) : 00.37%   : MEM_RESERVE
    
    Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
    

通常看到的更多是由临时大型对象导致的 VM 碎片,这些对象要求垃圾回收器频繁从操作系统获取新的托管堆段,并将空托管堆段释放回操作系统。

要验证 LOH 是否会生成 VM 碎片,可在 VirtualAlloc 和 VirtualFree 上设置一个断点,查看是谁调用了它们。 例如,如果想知道谁曾尝试从操作系统分配大于 8MBB 的虚拟内存块,可按以下方式设置断点:

控制台复制

bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"

只有在分配大小大于 8MB (0x800000) 的情况下调用 VirtualAlloc 时,此命令才会进入调试器并显示调用堆栈。

CLR 2.0 增加了称为“VM 囤积”的功能,用于频繁获取和释放段(包括在大型和小型对象堆上)的情况。 若要指定 VM 囤积,可通过托管 API 指定称为 STARTUP_HOARD_GC_VM 的启动标记。 CLR 退回这些段上的内存并将其添加到备用列表中,而不会将该空段释放回操作系统。 (请注意 CLR 不会针对太大型的段执行此操作。)CLR 稍后将使用这些段来满足新段请求。 下一次应用需要新段时,CLR 将使用此备用列表中的某个足够大的段。

VM 囤积还可用于想要保存已获取段的应用程序(例如属于系统上运行的主要应用的部分服务器应用),以避免内存不足的异常。

强烈建议你在使用此功能时认真测试应用程序,以确保应用程序的内存使用情况比较稳定。

垃圾回收通知

在有些情况下,公共语言运行时执行的完整垃圾回收(即第 2 代回收)可能会对性能产生负面影响。 特别是,处理大量请求的服务器可能会出现此问题;在这种情况下,长时间垃圾回收会导致请求超时。为了防止在关键时期发生完全回收,可以接收即将执行完全垃圾回收的通知,再采取措施将工作负载重定向到另一个服务器实例。 也可以自行诱导回收,前提是当前服务器实例不需要处理请求。

弱引用

强引用:如果应用程序的代码可以访问一个正由该程序使用的对象,垃圾回收器就不能回收该对象, 那么,就认为应用程序对该对象具有强引用。

弱引用允许应用程序访问对象,同时也允许垃圾回收器收集相应的对象。 如果不存在强引用,则弱引用的有限期只限于收集对象前的一个不确定的时间段

常问的问题:
什么样的对象需要垃圾回收?

​ 托管资源+引用类型

什么是托管资源和非托管资源?

​ 托管资源:托管的就是CLR控制的。包括new的对象,string字符串,变量

​ 非托管资源:非托管不是CLR能控制的,数据库连接,文件流,句柄,打印机连接

​ using(SqlConnection)被C#封装了管理了哪个非托管的数据库连接资源。

哪些对象能被GC回收?

​ 对象访问不到了,那就可以被回收了

​ 程序--入口--去找对象--建立对象图--访问不到的就是垃圾

对象时如何分配在堆上?

​ 连续分配在堆上面,每次分配就先检查空间够不够

什么时候执行GC?

​ new对象时--临界点

​ GC.Collect强制GC

​ 程序退出时会GC

参考资料

垃圾回收 MS

https://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html

http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html



这篇关于C#基础_垃圾回收(未完)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程