垃圾回收的基本知识
在公共语言运行时 (CLR) 中,垃圾回收器 (GC) 用作自动内存管理器。它提供如下优点:
使你可以在开发应用程序时不必手动释放内存。
有效分配托管堆上的对象。
回收不再使用的对象,清除它们的内存,并保留内存以用于将来分配。托管对象会自动获取干净的内容来开始,因此,它们的构造函数不必对每个数据字段进行初始化。
通过确保对象不能使用另一个对象的内容来提供内存安全。
本文章介绍垃圾回收的核心概念。
内存基础知识
下面的列表总结了重要的 CLR 内存概念。
每个进程都有其自己单独的虚拟地址空间。同一台计算机上的所有进程共享相同的物理内存和页文件(如果有)。
默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式虚拟地址空间。
作为一名应用程序开发人员,你只能使用虚拟地址空间,请勿直接操控物理内存。垃圾回收器为你分配和释放托管堆上的虚拟内存。
如果你编写的是本机代码,请使用 Windows 函数处理虚拟地址空间。这些函数为你分配和释放本机堆上的虚拟内存。
虚拟内存有三种状态:
可用。该内存块没有引用关系,可用于分配。
保留。内存块可供你使用,并且不能用于任何其他分配请求。但是,在该内存块提交之前,你无法将数据存储到其中。
提交。内存块已指派给物理存储。
可能会存在虚拟地址空间碎片。就是说地址空间中存在一些被称为孔的可用块。当请求虚拟内存分配时,虚拟内存管理器必须找到满足该分配请求的足够大的单个可用块。即使有 2GB 可用空间,2GB 分配请求也会失败,除非所有这些可用空间都位于一个地址块中。
如果没有足够的可供保留的虚拟地址空间或可供提交的物理空间,则可能会用尽内存。
即使在物理内存压力(即物理内存的需求)较低的情况下也会使用页文件。首次出现物理内存压力较高的情况时,操作系统必须在物理内存中腾出空间来存储数据,并将物理内存中的部分数据备份到页文件中。该数据只会在需要时进行分页,所以在物理内存压力较低的情况下也可能会进行分页。
垃圾回收的条件
当满足以下条件之一时将发生垃圾回收:
系统具有低的物理内存。这是通过 OS 的内存不足通知或主机指示的内存不足检测出来。
由托管堆上已分配的对象使用的内存超出了可接受的阈值。随着进程的运行,此阈值会不断地进行调整。
调用
System.GC.Collect
方法。几乎在所有情况下,你都不必调用此方法,因为垃圾回收器会持续运行。此方法主要用于特殊情况和测试。
托管堆
在垃圾回收器由 CLR 初始化之后,它会分配一段内存用于存储和管理对象。此内存称为托管堆(与操作系统中的本机堆相对)。
每个托管进程都有一个托管堆。进程中的所有线程都在同一堆上为对象分配内存。
若要保留内存,垃圾回收器会调用 Windows VirtualAlloc 函数,并且每次为托管应用保留一个内存段。垃圾回收器还会根据需要保留内存段,并调用 Windows VirtualFree 函数,将内存段释放回操作系统(在清除所有对象的内存段后)。
[IMPORTANT] 垃圾回收器分配的段大小特定于实现,并且随时可能更改(包括定期更新)。应用程序不应假设特定段的大小或依赖于此大小,也不应尝试配置段分配可用的内存量。
堆上分配的对象越少,垃圾回收器必须执行的工作就越少。分配对象时,请勿使用超出你需求的舍入值,例如在仅需要 15 个字节的情况下分配了 32 个字节的数组。
当触发垃圾回收时,垃圾回收器将回收由死对象占用的内存。回收进程会对活动对象进行压缩,以便将它们一起移动,并移除死空间,从而使堆更小一些。这将确保一起分配的对象全都位于托管堆上,从而保留它们的局部性。
垃圾回收的侵入性(频率和持续时间)是由分配的数量和托管堆上保留的内存数量决定的。
此堆可视为两个堆的累计:大对象堆和小对象堆。
大对象堆包含大小为 85,000 个字节和更多字节的大型对象。大对象堆上的对象通常是数组。非常大的实例对象是很少见的。
[TIP] 可以配置阈值大小,以使对象能够进入大型对象堆。
代数
堆按代进行组织,因此它可以处理长生存期的对象和短生存期的对象。垃圾回收主要在回收通常只占用一小部分堆的短生存期对象时发生。堆上的对象有三代:
第 0 代。这是最年轻的代,其中包含短生存期对象。短生存期对象的一个示例是临时变量。垃圾回收最常发生在此代中。
新分配的对象构成新一代对象,并隐式地成为第 0 代集合。但是,如果它们是大型对象,它们将延续到第 2 代集合中的大型对象堆。
大多数对象通过第 0 代中的垃圾回收进行回收,不会保留到下一代。
第 1 代。这一代包含短生存期对象并用作短生存期对象和长生存期对象之间的缓冲区。
第 2 代。这一代包含长生存期对象。长生存期对象的一个示例是服务器应用程序中的一个包含在进程期间处于活动状态的静态数据的对象。
当条件得到满足时,垃圾回收将在特定代上发生。回收某个代意味着回收此代中的对象及其所有更年轻的代。第 2 代垃圾回收也称为完整垃圾回收,因为它回收所有代上的所有对象(即,托管堆中的所有对象)。
幸存和提升
垃圾回收中未回收的对象也称为幸存者,并会被提升到下一代。在第 0 代垃圾回收中幸存的对象将被提升到第 1 代;在第 1 代垃圾回收中幸存的对象将被提升到第 2 代;而在第 2 代垃圾回收中幸存的对象将仍为第 2 代。
当垃圾回收器检测到某个代中的幸存率很高时,它会增加该代的分配阈值。下次回收将回收非常大的内存。CLR 持续在以下两个优先级之间进行平衡:不允许通过延迟垃圾回收,让应用程序的工作集获取太大内存,以及不允许垃圾回收过于频繁地运行。
暂时代和暂时段
因为第 0 代和第 1 代中的对象的生存期较短,因此,这些代被称为暂时代。
暂时代必须在称为暂时段的内存段中进行分配。垃圾回收器获取的每个新段将成为新的暂时段,并包含在第 0 代垃圾回收中幸存的对象。旧的暂时段将成为新的第 2 代段。
根据系统为 32 位还是 64 位以及它正在哪种类型的垃圾回收器上运行,暂时段的大小发生相应变化。下表列出了默认值。
||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 代对象可使用多个段(在内存允许的情况下进程所需的任意数量)。
从暂时垃圾回收中释放的内存量限制为暂时段的大小。释放的内存量与死对象占用的空间成比例。
垃圾回收过程中发生的情况
垃圾回收分为以下几个阶段:
标记阶段,找到并创建所有活动对象的列表。
重定位阶段,用于更新对将要压缩的对象的引用。
压缩阶段,用于回收由死对象占用的空间,并压缩幸存的对象。压缩阶段将垃圾回收中幸存下来的对象移至段中时间较早的一端。
因为第 2 代回收可以占用多个段,所以可以将已提升到第 2 代中的对象移动到时间较早的段中。可以将第 1 代幸存者和第 2 代幸存者都移动到不同的段,因为它们已被提升到第 2 代。
通常,由于复制大型对象会造成性能代偿,因此不会压缩大型对象堆 (LOH)。但是,在 .NET Core 和 .NET Framework 4.5.1 及更高版本中,可以根据需要使用
System.Runtime.GCSettings.LargeObjectHeapCompactionMode
属性按需压缩大型对象堆。此外,当通过指定以下任一项设置硬限制时,将自动压缩 LOH:针对容器的内存限制,或
[GCHeapHardLimit] 或 [GCHeapHardLimitPercent] 运行时配置选项
垃圾回收器使用以下信息来确定对象是否为活动对象:
堆栈根。由实时 (JIT) 编译器和堆栈查看器提供的堆栈变量。JIT 优化可以延长或缩短报告给垃圾回收器的堆栈变量内的代码的区域。
垃圾回收句柄。指向托管对象且可由用户代码或公共语言运行时分配的句柄。
静态数据。应用程序域中可能引用其他对象的静态对象。每个应用程序域都会跟踪其静态对象。
在垃圾回收启动之前,除了触发垃圾回收的线程以外的所有托管线程均会挂起。
下图演示了触发垃圾回收并导致其他线程挂起的线程。

操作非托管资源
如果托管对象使用非托管对象的本机文件句柄来引用非托管对象,则必须显式释放非托管对象,因为垃圾回收器仅跟踪托管堆上的内存。
托管对象的用户可能不会释放由该对象使用的本机资源。为了执行清理,可以使托管对象成为可终结的。终结由不再使用对象时执行的清理操作组成。当托管对象不活动时,它将执行在其终结器方法中指定的清理操作。
当发现某个可终结对象处于不活动状态时,则会将其终结器放入队列中,以便执行其清理操作,但要将该对象自身提升到下一代。因此,你必须等待该代上发生下一次垃圾回收(并不一定是下一次垃圾回收),以确定对象是否已收回。
有关终结版的详细信息,请参见 System.Object.Finalize
。
工作站和服务器垃圾回收
垃圾回收器可自行优化并且适用于多种方案。你可使用 配置文件设置 来基于工作负荷的特征设置垃圾回收的类型。CLR 提供了以下类型的垃圾回收:
工作站垃圾回收 (GC) 是为客户端应用为设计的。它是独立应用的默认 GC 风格。对于托管应用(例如由 ASP.NET 托管的应用),由主机确定默认 GC 风格。
工作站垃圾回收既可以是并发的,也可以是非并发的。并发垃圾回收使托管线程能够在垃圾回收期间继续操作。后台垃圾回收替换 .NET Framework 4 及更高版本中的并行垃圾回收。
服务器垃圾回收,用于需要高吞吐量和可伸缩性的服务器应用程序。
在 .NET Core 中,服务器垃圾回收既可以是非并发也可以是后台执行。
在 .NET Framework 4.5 及更高版本中,服务器垃圾回收可以非并行运行,还可在后台运行(后台垃圾回收可取代并行垃圾回收)。在 .NET Framework 4 和以前的版本中,服务器垃圾回收非并行运行。
下图演示了服务器上执行垃圾回收的专用线程:

比较工作站和服务器垃圾回收
以下是工作站垃圾回收的线程处理和性能注意事项:
回收发生在触发垃圾回收的用户线程上,并保留相同优先级。因为用户线程通常以普通优先级运行,所以垃圾回收器(在普通优先级线程上运行)必须与其他线程竞争 CPU 时间。(运行本机代码的线程不会由于服务器或工作站垃圾回收而挂起。)
工作站垃圾回收始终用于只有一个处理器的计算机,无论 配置设置 如何。
以下是服务器垃圾回收的线程处理和性能注意事项:
回收发生在以
THREAD_PRIORITY_HIGHEST
优先级运行的多个专用线程上。为每个 CPU 提供一个用于执行垃圾回收的一个堆和专用线程,并将同时回收这些堆。每个堆都包含一个小对象堆和一个大对象堆,并且所有的堆都可由用户代码访问。不同堆上的对象可以相互引用。
因为多个垃圾回收线程一起工作,所以对于相同大小的堆,服务器垃圾回收比工作站垃圾回收更快一些。
服务器垃圾回收通常具有更大的段。但是,这是通常情况:段大小特定于实现且可能更改。调整应用程序时,不要假设垃圾回收器分配的段大小。
服务器垃圾回收会占用大量资源。例如,假设在一台有 4 个处理器的计算机上,运行着 12 个使用服务器 GC 的进程。如果所有进程碰巧同时回收垃圾,它们会相互干扰,因为将在同一个处理器上调度 12 个线程。如果进程处于活动状态,则最好不要让它们都使用服务器 GC。
如果运行应用程序的数百个实例,请考虑使用工作站垃圾回收并禁用并发垃圾回收。这可以减少上下文切换,从而提高性能。
后台工作站垃圾回收
在后台工作站垃圾回收中,在进行第 2 代回收的过程中,将会根据需要收集暂时代(第 0 代和第 1 代)。后台工作站垃圾回收是在一个专用线程上执行的并且只适用于第 2 代回收。
默认启用后台垃圾回收,并且可以在 .NET Framework 应用程序中使用 gcConcurrent
配置设置或 .NET Framework 应用中的 System.GC.Concurrent
来启用或禁用后台垃圾回收。
[!NOTE] 后台垃圾回收替换在 .NET Framework 4 及更高版本中可用的并行垃圾回收。在 .NET Framework 4 中,仅支持工作站垃圾回收。从 .NET Framework 4.5 开始,后台垃圾回收可用于工作站和服务器垃圾回收。
后台垃圾回收期间对暂时代的回收称为前台垃圾回收。发生前台垃圾回收时,所有托管线程都将被挂起。
当后台垃圾回收正在进行并且你已在第 0 代中分配了足够的对象时,CLR 将执行第 0 代或第 1 代前台垃圾回收。专用的后台垃圾回收线程将在常见的安全点上进行检查以确定是否存在对前台垃圾回收的请求。如果存在,则后台回收将挂起自身以便前台垃圾回收可以发生。在前台垃圾回收完成之后,专用的后台垃圾回收线程和用户线程将继续。
后台垃圾回收可以消除并发垃圾回收所带来的分配限制,因为在后台垃圾回收期间,可发生暂时垃圾回收。后台垃圾回收可以删除暂存世代中的死对象。如果需要,它还可以在第 1 代垃圾回收期间扩展堆。
下图显示对工作站上的独立专用线程执行的后台垃圾回收:

后台服务器垃圾回收
从 .NET Framework 4.5 开始,后台服务器垃圾回收是服务器垃圾回收的默认模式。
后台服务器垃圾回收与后台工作站垃圾回收(如上一章节所描述)具有类似功能,但有一些不同之处:
后台工作区域垃圾回收使用一个专用的后台垃圾回收线程,而后台服务器垃圾回收使用多个线程。通常一个逻辑处理器有一个专用线程。
不同于工作站后台垃圾回收线程,这些线程不会超时。
下图显示对服务器上的独立专用线程执行的后台垃圾回收:

并行垃圾回收
[TIP] 本部分仅适用于:
用于工作站垃圾回收的 .NET Framework 3.5 及更早版本
用于服务器垃圾回收的 .NET Framework 4 及更早版本
在更高的版本中,后台垃圾回收取代了并行垃圾回收。
在工作站或服务器垃圾回收中,你可以启用并发垃圾回收,以便在大多数回收期间,让各线程与执行垃圾回收的专用线程并发运行。此选项只影响第 2 代中的垃圾回收;第 0 代和第 1 代中的垃圾回收始终是非并发的,因为它们完成的速度非常快。
并发垃圾回收通过最大程度地减少因回收引起的暂停,使交互应用程序能够更快地响应。在运行并发垃圾回收线程的大多数时间,托管线程可以继续运行。这可以使得在发生垃圾回收时的暂停时间更短。
并发垃圾回收在一个专用线程上执行。默认情况下,CLR 将运行工作站垃圾回收并启用并发垃圾回收。对于单处理器计算机和多处理器计算机都是如此。
下图演示了在单独的专用线程上执行的并发垃圾回收。

Reference
https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/fundamentals
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals




