垃圾回收的基础

在公共语言运行时 (CLR) 中,垃圾回收器用作自动内存管理器。它提供如下优点:

  • 使您可以在开发应用程序时不必释放内存。

  • 有效分配托管堆上的对象。

  • 回收不再使用的对象,清除它们的内存,并保留内存以用于将来分配。托管对象会自动获取干净的内容来开始,因此,它们的构造函数不必对每个数据字段进行初始化。

  • 通过确保对象不能使用另一个对象的内容来提供内存安全。

 

垃圾回收的条件

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

  • 系统具有低的物理内存。

  • 由托管堆上已分配的对象使用的内存超出了可接受的阈值。这意味着可接受的内存使用的阈值已超过托管堆。随着进程的运行,此阈值会不断地进行调整。

  • 调用 GC.Collect 方法。几乎在所有情况下,您都不必调用此方法,因为垃圾回收器会持续运行。此方法主要用于特殊情况和测试。

 

托管堆

在由 CLR 进行初始化之后,垃圾回收器将分配内存段来保存和管理对象。此内存称为托管堆(与操作系统中的本机堆相对)。

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

若要保留内存,垃圾回收器将调用 Win32 VirtualAlloc 函数,并且每次会为托管应用程序保留一个内存段。垃圾回收器还将根据需要保留一些内存段,并通过调用 Win32 VirtualFree 函数将这些内存段释放回到操作系统(在清除任何对象的内存段之后)。

堆上分配的对象越少,垃圾回收器必须执行的工作就越少。当分配对象时,不会向上舍入到超过您的需要的值,例如,当只需要 15 字节时分配一个 32 字节的数组。

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

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

此堆可视为两个堆的累计:大对象堆和小对象堆。

大对象堆包含其大小为 85,000 个字节和更多字节的对象。大对象堆上的特大对象通常是数组。非常大的实例对象是很少见的。

 

代数

堆按代进行组织,因此它可以处理长生存期的对象和短生存期的对象。垃圾回收主要在回收通常只占用一小部分堆的短生存期对象时发生。堆中存在三代对象:

  • 第 0 代

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

    新分配的对象构成新一代的对象并且为隐式的第 0 代回收,除非它们是大对象,在这种情况下,它们将进入第 2 代回收中的大对象堆。

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

  • 第 1 代

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

  • 第 2 代

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

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

幸存和提升

垃圾回收中未回收的对象也称为幸存者,并会被提升到下一代。在第 0 代垃圾回收中幸存的对象将被提升到第 1 代;在第 1 代垃圾回收中幸存的对象将被提升到第 2 代;而在第 2 代垃圾回收中幸存的对象将仍为第 2 代。

当垃圾回收器检测到某个代中的幸存率很高时,它会增加该代的分配阈值,因此下一次回收将会获取一个非常大的回收内存。CLR 会在以下两个优先级别之前进行平衡:不允许应用程序的工作集获取太大内存以及不允许垃圾回收花费太多时间。

暂时代和暂时段

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

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

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

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

 

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

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

  • 标记阶段,用于查找所有活动的对象并标记它们。

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

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

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

    将不会压缩大对象堆,因为这会在一个不可接受的时间长度内增加内存使用量。

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

  • 堆栈根

    由实时 (JIT) 编译器和堆栈查看器提供的堆栈变量。

  • 垃圾回收句柄

    这些句柄指向托管对象并且可由用户代码或公共语言运行时进行分配。

  • 静态数据

    应用程序域中可引用其他对象的静态数据。每个应用程序域都会跟踪其静态对象。

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

下图演示了一个触发垃圾回收的线程处理导致了其他线程均被挂起。

触发垃圾回收的线程

 

操作非托管资源

如果托管对象通过使用其本机文件句柄来引用未托管对象,则必须显式释放这些托管对象,因为垃圾回收器只在托管堆上跟踪内存。

托管对象的用户可能不会释放由该对象使用的本机资源。为了执行清理,可以使托管对象成为可终结的。终结由不再使用对象时执行的清理操作组成。当托管对象不活动时,它将执行在其终结器方法中指定的清理操作。

当发现某个可终结对象处于不活动状态时,则会将其终结器放入队列中,以便执行其清理操作,但要将该对象自身提升到下一代。因此,必须等待至在下一代上发生的下一次垃圾回收(不一定是下一次垃圾回收),以确定是否由垃圾回收功能回收对象。

 

工作站和服务器垃圾回收

垃圾回收器可自行优化并且适用于多种方案。根据工作负荷的特征,唯一可以设置的选项是垃圾回收的类型。CLR 提供了以下类型的垃圾回收:

  • 工作站垃圾回收,用于所有客户端工作站和独立 PC。这是运行时配置架构中的 <gcServer> 元素的默认设置。

    工作站垃圾回收既可以是并发的,也可以是非并发的。并发垃圾回收使托管线程能够在垃圾回收期间继续操作。

    从 .NET Framework 4 版开始,后台垃圾回收替代了并发垃圾回收。 

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

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

服务器垃圾回收

配置垃圾回收

可以使用运行时配置架构的 <gcServer> 元素指定您想要 CLR 执行的垃圾回收的类型。在将此元素的 enabled 特性设置为 false(默认值)时,CLR 将执行工作站垃圾回收。在将 enabled 特性设置为 true 时,CLR 将执行服务器垃圾回收。

可使用运行时配置架构的 <gcConcurrent> 元素指定并发垃圾回收。默认设置为 enabled并发垃圾回收只可用于工作站垃圾回收并���对服务器垃圾回收没有影响。

还可以使用非托管承载接口来指定服务器垃圾回收。请注意,对于在 ASP.NET 和 Microsoft SQL Server 2005 中承载的应用程序,将会自动启用服务器垃圾回收。

有关工作站和服务器垃圾回收比较的注意事项

工作站垃圾回收具有以下线程和性能方面的注意事项:

  • 回收发生在触发垃圾回收的用户线程上,并保留相同优先级。因为用户线程通常以普通优先级运行,所以垃圾回收器(在普通优先级线程上运行)必须与其他线程竞争 CPU 时间。

    运行本机代码的线程不会被挂起。

  • 工作站垃圾回收始终用在只有一个处理器的计算机上,而不管 <gcServer> 设置如何。如果指定服务器垃圾回收,则 CLR 将使用工作站垃圾回收并禁用并发垃圾回收。

服务器垃圾回收具有以下线程和性能方面的注意事项:

  • 回收发生在以 THREAD_PRIORITY_HIGHEST priority level 优先级别运行的多个专用线程上。

  • 为每个 CPU 提供一个用于执行垃圾回收的专用线程和一个堆,并将同时回收这些堆。每个堆都包含一个小对象堆和一个大对象堆,并且所有的堆都可由用户代码访问。不同堆上的对象可以相互引用。

  • 因为多个垃圾回收线程一起工作,所以对于相同大小的堆,服务器垃圾回收比工作站垃圾回收更快一些。

  • 服务器垃圾回收通常具有更大的段。

  • 服务器垃圾回收会占用大量资源。例如,如果在一台具有 4 个处理器的计算机上运行了 12 个进程,则在它们都使用服务器垃圾回收的情况下,将有 48 个专用垃圾回收线程。在高内存加载的情况下,如果所有进程开始执行垃圾回收,则垃圾回收器将要计划 48 个线程。

如果运行应用程序的数百个实例,请考虑使用工作站垃圾回收并禁用并发垃圾回收。这可以减少上下文切换,从而提高性能。

返回页首

并发垃圾回收

在工作站垃圾回收中,可以启用并发垃圾回收,这将允许线程在垃圾回收的大部分时间内与执行垃圾回收的专用线程并发运行。此选项只影响第 2 代中的垃圾回收;第 0 代和第 1 代中的垃圾回收始终是非并发的,因为它们完成的速度非常快。

并发垃圾回收通过最大程度地减少因回收引起的暂停,使交互应用程序能够更快地响应。在运行并发垃圾回收线程的大多数时间,托管线程可以继续运行。这可以使得在发生垃圾回收时的暂停时间更短。

若要在运行多个进程时提高性能,请禁用并发垃圾回收。

并发垃圾回收在一个专用线程上执行。默认情况下,CLR 将运行工作站垃圾回收并启用并发垃圾回收。对于单处理器计算机和多处理器计算机都是如此。

您在并发垃圾回收期间在堆上为小对象分配空间的能力将受到在并发垃圾回收启动时暂时段上保留的对象的限制。一旦到达暂时段的末尾,将必须等待并发垃圾回收完成,同时将挂起需要执行小对象分配的托管线程。

并发垃圾回收具有一个稍微大点的工作集(与非并发垃圾回收相比),这是因为您可以在并发回收期间分配对象。但是,这会影响性能,原因是分配的对象将会成为您的工作集的一部分。实质上,并发垃圾回收会牺牲一些 CPU 和内存来换取更短的暂停。

下图演示了在单独的专用线程上执行的并发垃圾回收。

并行垃圾回收

 

后台垃圾回收

在后台垃圾回收中,在进行第 2 代回收的过程中,将会根据需要收集暂时代(第 0 代和第 1 代)。后台垃圾回收无法设置;它会自动运行并启用并发垃圾回收。后台垃圾回收是对并发垃圾回收的替代。与并发垃圾回收一样,后台垃圾回收是在一个专用线程上执行的并且只适用于第 2 代回收。

说明说明

后台垃圾回收只在 .NET Framework 4 及更高版本中可用。

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

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

后台垃圾回收移除了由并发垃圾回收施加的分配限制,这是因为暂时垃圾回收会在后台垃圾回收期间发生。这意味着,后台垃圾回收可以移除暂时代中的死对象,而且还可以在第 1 代垃圾回收期间根据需要展开堆。

后台垃圾回收当前不可用于服务器垃圾回收。

下图演示了一个前台垃圾回收正在运行情况下的后台垃圾回收。

后台垃圾回收
posted @ 2010-06-03 14:41  zhh  阅读(239)  评论(0编辑  收藏  举报