改进垃圾回收性能

使用 C# 和 Visual Basic 编写的通用 Windows 平台 (UWP) 应用从 .NET 垃圾回收器获取自动内存管理。 本部分汇总了 UWP 应用中的 .NET 垃圾回收器的行为和性能最佳实践。 有关 .NET 垃圾回收器的工作原理以及调试和分析垃圾回收器性能的工具的详细信息,请参阅垃圾回收

注意 需要干涉垃圾回收器的默认行为是应用的一般内存问题的显著特征。 有关详细信息,请参阅在 Visual Studio 2015 中调试时使用的内存使用工具。 本主题仅适用于 C# 和 Visual Basic。

 

垃圾回收器通过平衡托管堆的内存消耗与垃圾回收需要执行的工作量来确定运行时间。 垃圾回收器执行此操作的其中一种方式是将堆栈划分为几代且大部分时间仅回收堆栈的一部分。 在托管堆栈中有三代:

  • 第 0 代。 这一代包含新分配的对象,除非它们为 85KB 或更大,在这种情况下,它们将属于大型对象堆栈。 大型对象堆栈使用第 2 代回收进行回收。 第 0 代回收是最经常出现的回收类型,它们清理生存期短的对象,如本地变量。
  • 第 1 代。 这一代包含从第 0 代回收留存下来的对象。 这一代充当第 0 代和第 2 代之间的缓冲区。 第 1 代回收没有第 0 代回收出现得那么频繁,它们清理在先前的第 0 代回收期间处于活动状态的临时对象。 第 1 代回收也回收第 0 代。
  • 第 2 代。 这一代包含从第 0 代和第 1 代回收留存下来的生存期长的对象。 第 2 代回收出现频率最少,它们回收整个托管堆栈,包括大型对象堆栈(包含 85KB 或更大的对象)。

可以从两个方面来衡量垃圾回收器的性能:执行垃圾回收所花费的时间和托管堆栈的内存消耗。 如果你有一个小型应用,且堆栈大小小于 100MB,请专注于减少内存消耗。 如果你的应用的托管堆栈大于 100M,请仅专注于减少垃圾回收时间。 下面介绍如何帮助 .NET 垃圾回收器实现更好的性能。

减少内存消耗

释放引用

对应用中的某个对象的引用会禁止回收该对象以及该对象引用的所有对象。 .NET 实时编译器可以很好地检测何时不再使用某个变量,以便该变量所保留的对象将可以回收。 但在某些情况下,虽然某些对象包含对其他对象的引用,但这种关系可能不是很明显,因为对象图的一部分可能归应用使用的库所有。 要了解用于查明哪些对象在垃圾回收之后还会留存下来的工具和技术,请参阅垃圾回收和性能

引发垃圾回收(如果有用)

仅当你已衡量应用的性能且已确定引发回收将改进其性能之后,才引发垃圾回收。

可以通过调用 GC.Collect(n) 来引发某一代的垃圾回收,其中 n 是要回收的代(0、1 或 2)。

注意 我们建议,不要强制在应用中进行垃圾回收,因为垃圾回收器使用许多启发式技术来确定执行回收的最佳时间,且强制进行回收在许多情况下会造成不必要的 CPU 消耗。 但是,如果你知道自己的应用中有大量不再使用的对象,并且你希望将此内存返回给系统,此时则适合强制进行垃圾回收。 例如,在游戏中,在某个加载序列结束时你可以引发回收,以在游戏开始之前释放内存。   为了避免意外引发过多垃圾回收,可以将 GCCollectionMode 设置为 Optimized。 这会指导垃圾回收器仅在确定回收将足够富有成效,可证明其合理性时才启动回收。

缩短垃圾回收时间

如果你已分析应用并观察到大量垃圾回收时间,此部分将适用。 与垃圾回收相关的暂停时间包括:运行单一垃圾回收过程所花费的时间;以及应用执行垃圾回收花费的总时间。 进行回收所花费的时间取决于回收器必须分析的实时数据量。 第 0 代和第 1 代在大小上是有限制的,但随着越来越多的长生存期对象在你的应用中处于活动状态,第 2 代会不断增多。 这意味着第 0 代和第 1 代的回收时间是有限制的,而第 2 代回收有可能会花费较长的时间。 垃圾回收运行的频率大多取决于你分配的内存量,因为垃圾回收通过释放内存来满足分配请求。

垃圾回收有时会使你的应用暂停执行工作,但并不需要在它执行回收的整个时间内都暂停你的应用。 用户通常在你的应用中觉察不到暂停时间,特别是对于第 0 代和第 1 代回收。 .NET 垃圾回收器的后台垃圾回收功能允许在你的应用运行的同时并发执行第 2 代回收,并且仅会使你的应用暂停很短的一段时间。 但并非始终可以将第 2 代回收作为后台回收执行。 在这种情况下,如果你有足够大的堆栈(超过 100MB),那么用户将可以觉察到暂停。

频繁的垃圾回收可能会导致 CPU 消耗增加(并因此导致功率消耗增加)、加载时间更长或应用程序中的帧速率降低。 可使用下列技术缩短垃圾回收时间、减少托管 UWP 应用中与回收相关的暂停。

减少内存分配

如果不分配任何对象,那么垃圾回收器就不会运行,除非系统中存在低内存条件。 减少分配的内存的数量将直接导致垃圾回收频率降低。

如果在你的应用的某些部分中,暂停完全不可取,那么你可以在某段性能不是很关键的时间内事先预分配必要的对象。 例如,游戏可能会在某个级别的加载屏幕期间分配进行游戏所需的所有对象,并且在游戏进行期间不进行任何分配。 这样可避免用户正在玩游戏时出现暂停,并可能会使帧速率更高、更一致。

通过避免具有中等长度生存期的对象,减少第 2 代回收

当你的应用中有真正短生存期和/或真正长生存期的对象时,分代垃圾回收执行得最好。 短生存期对象在开销较少的第 0 代和第 1 代回收中被回收,长生存期对象被提升至不经常进行回收的第 2 代。 长生存期对象是用于你的应用的整个持续时间的那些对象,或者用于你的应用的重要期间(如在某个特定页面或游戏级别期间)的那些对象。

如果你经常创建具有临时生存时间但生存时间很长足以提升至第 2 代的对象,将会发生更多耗费资源的第 2 代回收。 你也许能够通过回收现有对象或更快地释放对象来减少第 2 代回收。

具有中期生存时间的对象的常见示例是用于显示用户滚动查看的列表中的项。 如果对象是在将列表中的项滚动到视图中时创建的,且随着列表中的项滚动到视图之外而不再被引用,那么应用通常有大量第 2 代回收。 在此类情形下,你可以为主动显示给用户的数据预分配和重新使用一组对象,并随着列表中的项进入视图中后使用短生存期对象来加载信息。

通过避免使用生存期短的大型对象来减少第 2 代回收

任何 85KB 或更大的对象将分配在大型对象堆栈 (LOH) 上并作为第 2 代的一部分进行回收。 如果你有大于 85KB 的临时变量(例如缓冲区),第 2 代回收将会清理它们。 将临时变量限制为小于 85KB 会减少应用中第 2 代集合的数量。 一项常见的技术是创建缓冲池并重新使用池中的对象以避免大量临时分配。

避免引用丰富的对象

垃圾回收器通过追踪对象之间的引用(从你的应用中的根开始)来确定哪些对象是实时的。 有关详细信息,请参阅垃圾回收期间发生了什么情况。 如果某个对象包含许多引用,那么垃圾回收器将有更多工作要做。 一项常见的技术(特别是对于大型对象)是将引用丰富的对象转换为无引用的对象(例如,不是存储引用,而是存储索引)。 当然,此技术仅适合逻辑上可能这样做时。

将对象引用替换为索引可能是对应用的一种破坏性复杂更改,且对于包含大量引用的大型对象最有效。 仅当你注意到在你的应用中存在与引用频繁的对象相关的大量垃圾回收时间时,才这样做。