垃圾回收和性能

本主题介绍与垃圾回收和内存使用有关的问题。 本文解决与托管堆有关的问题并说明如何尽量减少垃圾回收对应用程序的影响。 每个问题都有指向可用于调查问题的过程的链接。

本主题包含以下各节:

  • 性能分析工具

  • 解决性能问题

  • 问题排查指导

  • 性能检查过程

性能分析工具

以下各节介绍可用于调查内存使用和垃圾回收问题的工具。 本主题后面提供的过程将引用这些工具。

内存性能计数器

您可以使用性能计数器收集性能数据。 有关说明,请参见运行时分析。 性能计数器的 .NET CLR Memory 类别(如内存性能计数器中所述)提供有关垃圾回收器的信息。

具有 SOS 的调试器

您可以使用具有 SOS.dll(SOS 调试扩展)Windows Debugger (WinDbg)(Windows 调试器 (WinDbg))或 Visual Studio 调试器来检查托管堆上的对象。

若要安装 WinDbg,可从 WDK and Developer Tools Web site(WDK 和开发人员工具网站)安装 Windows 调试工具。 有关使用 SOS Debugging Extension 的信息,请参见如何:使用 SOS

垃圾回收 ETW 事件

Windows (ETW) 事件跟踪是一个跟踪系统,用于辅助 .NET Framework 提供的分析和调试支持。 从 .NET Framework 4 版开始,垃圾回收 ETW 事件可从统计的角度捕获用于分析托管堆的有用信息。 例如,GCStart_V1 事件(垃圾回收即将发生时引发)提供以下信息:

  • 将回收哪一代对象。

  • 触发垃圾回收的原因。

  • 垃圾回收的类型(并发或非并发)。

ETW 事件日志很高效,且不会掩盖任何与垃圾回收相关的性能问题。 进程可以提供自己的事件以及 ETW 事件。 记录时,可以结合应用程序事件和垃圾回收事件以确定堆问题是如何以及何时发生的。 例如,服务器应用程序可以在客户端请求开始和结束时提供事件。

分析 API

公共语言运行时 (CLR) 分析接口提供有关垃圾回收期间受影响的对象的详细信息。 垃圾回收开始和结束时可以通知探查器。 它可以提供有关托管堆上对象的报告,包括每代中对象的标识。 有关更多信息,请参见分析 API 中的对象跟踪

探查器可以提供全面的信息。 但是,复杂的探查器可能会修改应用程序的行为。

应用程序域资源监控

从 .NET Framework 4 开始,应用程序域资源监控 (ARM) 允许主机通过应用程序域监控 CPU 和内存使用率。 有关更多信息,请参见应用程序域资源监控

返回页首

解决性能问题

第一步是确定问题是否真的为垃圾回收。 如果确定是,请从下表中进行选择以排查问题。

  • 引发内存不足的异常

  • 进程使用过多的内存

  • 垃圾回收器回收对象的速度不够快

  • 托管堆太零碎

  • 垃圾回收暂停时间过长

  • 第 0 代过大

  • 垃圾回收期间 CPU 使用率过高

问题:引发内存不足的异常

正常情况下,引发托管的 OutOfMemoryException 有两种原因:

  • 虚拟内存不足。

    垃圾回收器按照预先确定大小的段分配系统内存。 如果分配需要其他段,但是进程的虚拟内存空间中没有剩余的连续可用块,则托管堆的分配将失败。

  • 没有足够的物理内存用于分配。

性能检查

确定是否已托管内存不足异常。

确定可保留多少虚拟内存。

确定是否有足够的物理内存。

如果您确定该异常不合理,请联系 Microsoft 客户服务和支持部门,并提供以下信息:

  • 发生托管内存不足异常的堆栈。

  • 完整内存转储。

  • 证明其不是正常的内存不足异常的数据,包括指示虚拟或物理内存不是问题的数据。

问题:进程使用的内存过多

一个常见的假设就是 Windows 任务管理器**“性能”**选项卡上的内存使用率显示可以指示何时使用了大量内存。 但是,该显示与工作集有关,它不提供有关虚拟内存使用率的信息。

如果您确定问题是托管堆引起的,则必须随时测量托管堆以确定任何模式。

如果您确定问题不是托管堆引起的,则必须使用本机调试。

性能检查

确定可保留多少虚拟内存。

确定托管堆将提交多少内存。

确定托管堆保留了多少内存。

确定第 2 代中的大型对象。

确定对象的引用。

问题:垃圾回收器回收对象的速度不够快

当似乎未按照垃圾回收预期的样子回收对象时,必须确定是否对这些对象进行了任何强引用。

如果未对包含弃用对象的代进行垃圾回收(即指示弃用对象的终结器未运行),则也可能会遇到此问题。 例如,当您运行单线程单元 (STA) 应用程序,并且为终结器队列提供服务的线程无法调用它时,可能会发生这种情况。

性能检查

检查对象的引用。

确定终结器是否已运行。

确定是否存在等待终结的对象。

问题:托管堆太零碎

碎片级别按可用空间与为该代分配的总内存的比值进行计算。 对于第 2 代,可接受的碎片级别不能超过 20%。 由于第 2 代可能非常大,碎片的比值比绝对值更为重要。

第 0 代中具有很多可用空间并不是问题,因为这是分配新对象的一代。

碎片始终出现在大型对象堆中,因为其未压缩。 相邻的自由对象将自然地合并成一个空间以满足大型对象分配请求。

碎片在第 1 代和第 2 代中可能成为问题。 如果这些代在垃圾回收之后具有大量的可用空间,则可能需要修改应用程序的对象使用率,并且您应考虑重新计算长期对象的生存期。

固定过多对象可能会增加碎片的数量。 如果碎片太多,表明可能固定了过多对象。

如果虚拟内存的碎片阻止垃圾器回收添加段,则可能为以下原因之一:

  • 频繁载入和卸载许多小型程序集。

  • 与非托管代码互操作时保留了过多对 COM 对象的引用。

  • 创建大型临时对象,这会导致大型对象堆频繁分配和释放堆段。

    托管 CLR 时,应用程序可以请求垃圾回收器保留它的段。 这会减少段分配的频率。 可使用 STARTUP_FLAGS 枚举 中的 STARTUP_HOARD_GC_VM 标志来完成此操作。

性能检查

确定托管堆中可用空间的大小。

确定固定对象的数量。

如果您认为碎片出现的原因不合理,请与 Microsoft 客户服务和支持部门联系。

问题:垃圾回收暂停时间过长

垃圾回收按照软实时进行运行,所以应用程序必须能够接受某些暂停。 软实时的一个条件就是 95% 的操作必须按时完成。

在并发垃圾回收中,允许托管的线程在回收期间运行,这意味着暂停非常少。

短期垃圾回收(第 0 代和第 1 代)仅持续几毫秒,所以减少暂停通常不可行。 但是,您可以通过更改应用程序分配请求的模式来减少第 2 代回收中的暂停。

另一个更精确的方式是使用垃圾回收 ETW 事件。 您可以通过为一系列事件添加时间戳差异来查找回收的计时。 整个回收序列包括执行引擎的挂起、垃圾回收本身,以及执行引擎的恢复。

您可以使用垃圾回收通知确定服务器是否即将执行第 2 代回收,以及另一台服务器的重导请求是否可以解决有关暂停的所有问题。

性能检查

确定垃圾回收的时间。

确定引发垃圾回收的原因。

问题:第 0 代过大

第 0 代在 64 位系统上可能具有大量对象,特别是当您使用服务器垃圾回收,而非工作站垃圾回收时。 这是因为触发第 0 代垃圾回收的阈值高于这些环境,因此第 0 代回收可能会大很多。 如果应用程序在触发垃圾回收之前分配更多内存,则可提升性能。

问题:垃圾回收期间 CPU 使用率过高

垃圾回收期间 CPU 使用率会很高。 如果垃圾回收所用的处理时间非常长,则回收的次数会过于频繁或者回收持续时间过长。 增加托管堆上对象的分配比率可能导致发生垃圾回收的频率更高。 降低分配比率会降低垃圾回收的频率。

您可以使用 Allocated Bytes/second 性能计数器监控分配比率。 有关更多信息,请参见内存性能计数器

回收的持续时间是分配后幸存对象数量的一个主要因素。 如果有许多对象要回收,则垃圾回收器必须浏览大部分内存。 压缩幸存对象的工作非常耗时。 若要确定回收期间处理了多少对象,请将调试器的断点设置为指定代垃圾回收结束的时间点。

性能检查

确定高 CPU 使用率是否是由垃圾回收引起的。

将断点设置为垃圾回收结束的时间点。

返回页首

问题排查指导

本节介绍您开始调查时应考虑的相应指导。

工作站或服务器垃圾回收

确定您是否使用了正确类型的垃圾回收。 如果应用程序使用多个线程和对象实例,请使用服务器垃圾回收,而非工作站垃圾回收。 服务器垃圾回收使用多个线程,而工作站垃圾回收需要多个应用程序实例运行它们自己的垃圾回收线程并争用 CPU 时间。

负载低且不常在后台执行任务的应用程序(例如服务)可以使用工作站垃圾回收,并禁用并发垃圾回收。

何时测量托管堆的大小

除非您使用的是探查器,否则必须建立一致的测量模式以有效诊断性能问题。 制定计划时请考虑以下几点:

  • 如果您在第 2 代垃圾回收之后测量,则会清除整个托管堆的垃圾(弃用对象)。

  • 如果您在第 0 代垃圾回收之后立即测量,则第 1 代和第 2 代中的对象尚未得到回收。

  • 如果您在垃圾回收之前即开始测量,则将在垃圾回收开始之前测量尽可能多的分配。

  • 垃圾回收期间的测量是有问题的,因为垃圾回收器数据结构处于遍历的无效状态,并可能无法提供完整的结果。 这是设计使然。

  • 当您使用带并发垃圾回收的工作站垃圾回收时,不会压缩回收的对象,所以堆大小可能保持不变或变大(碎片会使其似乎更大)。

  • 当物理内存负载过高时,第 2 代上的并发垃圾回收会延迟。

下面的步骤介绍如何设置断点,以便您可以测量托管堆。

将断点设置为垃圾回收结束的时间点

  • 在加载了 SOS debugger extension 的 WinDbg 中,键入以下命令:

    bp mscorwks!WKS::GCHeap::RestartEE "j (dwo(mscorwks!WKS::GCHeap::GcCondemnedGeneration)==2) 'kb';'g'"

    其中 GcCondemnedGeneration 应设置为所需的代。 此命令要求使用私有符号。

    如果在对第 2 代对象进行垃圾回收之后执行 RestartEE,则此命令会强制中断。

    在服务器垃圾回收中,只有一个线程调用 RestartEE,所以在第 2 代垃圾回收期间只会出现一次断点。

返回页首

性能检查过程

本节介绍以下用于确定性能问题原因的过程:

  • 确定问题是否由垃圾回收引起。

  • 确定是否已托管内存不足异常。

  • 确定可保留多少虚拟内存。

  • 确定是否有足够的物理内存。

  • 确定托管堆将提交多少内存。

  • 确定托管堆保留了多少内存。

  • 确定第 2 代中的大型对象。

  • 确定对象的引用。

  • 确定终结器是否已运行。

  • 确定是否存在等待终结的对象。

  • 确定托管堆中可用空间的大小。

  • 确定固定对象的数量。

  • 确定垃圾回收的时间。

  • 确定触发垃圾回收的原因。

  • 确定高 CPU 使用率是否由垃圾回收引起。

确定问题是否由垃圾回收引起。

  • 检查以下两个内存性能计数器:

    • 垃圾回收占用的时间百分比。 显示上次垃圾回收周期后执行垃圾回收所用时间的百分比。 使用该计数器确定垃圾回收器是否花费过多时间来使托管堆空间可用。 如果垃圾回收占用的时间相对较少,则指示托管堆外可能存在资源问题。 当包括并发或后台垃圾回收时,该计数器可能不准确。

    • 提交的总字节数。 显示垃圾回收器当前提交的虚拟内存量。 使用该计数器可确定垃圾回收器所消耗的内存是否占应用程序所使用内存的大部分。

    大多数内存性能计数器都会在每次垃圾回收结束时更新。 因此,它们可能不反映您需要其相关信息的当前条件。

确定是否已托管内存不足异常

  1. 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入打印异常 (pe) 命令:

    !pe

    如果已托管异常,则 OutOfMemoryException 将显示为异常类型,如下面的示例所示。

    Exception object: 39594518
    Exception type: System.OutOfMemoryException
    Message: <none>
    InnerException: <none>
    StackTrace (generated):
    
  2. 如果输出不指定异常,那么您必须确定内存不足异常来自哪个线程。 在调试器中键入下面的命令以显示所有线程及其调用堆栈:

    ~*kb

    堆栈具有异常调用的线程由 RaiseTheException 参数指示。 这是托管异常对象。

    28adfb44 7923918f 5b61f2b4 00000000 5b61f2b4 mscorwks!RaiseTheException+0xa0 
    
  3. 您可以使用以下命令转储嵌套的异常。

    !pe -nested

    如果您未发现任何异常,则内存不足异常源自非托管代码。

确定可保留多少虚拟内存

  • 在加载了 SOS debugger extension 的 WinDbg 中,键入以下命令以获得最大可用区域:

    !address -summary

    最大可用区域如下面的输出所示。

    Largest free region: Base 54000000 - Size 0003A980
    

    在本示例中,最大可用区域的大小约为 24000 KB(十六进制为 3A980)。 该区域与垃圾回收器针对段所需的大小相比,要小得多。

    - 或 -

  • 使用 vmstat 命令:

    !vmstat

    最大可用区域是 MAXIMUM 列中的最大值,如下面的输出所示。

    TYPE        MINIMUM   MAXIMUM     AVERAGE   BLK COUNT   TOTAL
    ~~~~        ~~~~~~~   ~~~~~~~     ~~~~~~~   ~~~~~~~~~~  ~~~~
    Free:
    Small       8K        64K         46K       36          1,671K
    Medium      80K       864K        349K      3           1,047K
    Large       1,384K    1,278,848K  151,834K  12          1,822,015K
    Summary     8K        1,278,848K  35,779K   51          1,824,735K
    

确定是否有足够的物理内存

  1. 启动 Windows 任务管理器。

  2. 在**“性能”选项卡上,查看提交的值。 (在 Windows 7 中,查看“系统组”中的“提交(KB)”**。)

    如果**“总数”接近于“限制”**,表明您的物理内存即将用尽。

确定托管堆将提交多少内存

  • 使用 # Total committed bytes 内存性能计数器可获取托管堆将提交的字节数。 垃圾回收器根据需要提交段上的区块,并不是同时全部提交。

    注意注意

    请勿使用 # Bytes in all Heaps 性能计数器,因为它不表示托管堆的实际内存使用率。代的大小包括在该值中,并且实际上就是其阈值大小,即如果该代已填满对象,则会引发垃圾回收。因此,该值通常为 0。

确定托管堆保留了多少内存

  • 使用 # Total reserved bytes 内存性能计数器。

    垃圾回收器在段中保留内存,您可以使用 eeheap 命令确定段开始的位置。

  • 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入以下命令:

    !eeheap -gc

    结果如下所示。

    Number of GC Heaps: 2
    ------------------------------
    Heap 0 (002db550)
    generation 0 starts at 0x02abe29c
    generation 1 starts at 0x02abdd08
    generation 2 starts at 0x02ab0038
    ephemeral segment allocation context: none
     segment    begin allocated     size
    02ab0000 02ab0038  02aceff4 0x0001efbc(126908)
    Large object heap starts at 0x0aab0038
     segment    begin allocated     size
    0aab0000 0aab0038  0aab2278 0x00002240(8768)
    Heap Size   0x211fc(135676)
    ------------------------------
    Heap 1 (002dc958)
    generation 0 starts at 0x06ab1bd8
    generation 1 starts at 0x06ab1bcc
    generation 2 starts at 0x06ab0038
    ephemeral segment allocation context: none
     segment    begin allocated     size
    06ab0000 06ab0038  06ab3be4 0x00003bac(15276)
    Large object heap starts at 0x0cab0038
     segment    begin allocated     size
    0cab0000 0cab0038  0cab0048 0x00000010(16)
    Heap Size    0x3bbc(15292)
    ------------------------------
    GC Heap Size   0x24db8(150968)
    

    由“段”指示的地址是段的起始地址。

确定第 2 代中的大型对象

  • 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入以下命令:

    !dumpheap –stat

    如果托管堆很大,则 dumpheap 可能需要一些时间才能完成。

    您可以从输出的最后几行处开始分析,因为它们列出了使用最多空间的对象。 例如:

    2c6108d4   173712     14591808 DevExpress.XtraGrid.Views.Grid.ViewInfo.GridCellInfo
    00155f80      533     15216804      Free
    7a747c78   791070     15821400 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700930     19626040 System.Collections.Specialized.ListDictionary
    2c64e36c    78644     20762016 DevExpress.XtraEditors.ViewInfo.TextEditViewInfo
    79124228   121143     29064120 System.Object[]
    035f0ee4    81626     35588936 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    791242ec    40182     90664128 System.Collections.Hashtable+bucket[]
    790fa3e0  3154024    137881448 System.String
    Total 8454945 objects
    

    列出的最后一个对象是字符串,它占用最多的空间。 您可以检查您的应用程序以查看如何优化字符串对象。 若要查看 150 到 200 字节之间的字符串,请键入以下内容:

    !dumpheap -type System.String -min 150 -max 200

    结果的示例如下所示。

    Address  MT           Size  Gen
    1875d2c0 790fa3e0      152    2 System.String HighlightNullStyle_Blotter_PendingOrder-11_Blotter_PendingOrder-11
    …
    

    对 ID 使用整数而非字符串会更高效。 如果相同的字符串重复了上千次,请考虑字符串驻留。 有关字符串驻留的更多信息,请参见 String.Intern 方法的参考主题。

确定对象的引用

  • 在加载了 SOS debugger extension 的 WinDbg 中,键入以下命令以列出对象的引用:

    !gcroot

    -or-

  • 若要确定特定对象的引用,请包括地址:

    !gcroot 1c37b2ac

    堆栈上找到的根可能为误报。 有关更多信息,请使用 !help gcroot 命令。

    ebx:Root:19011c5c(System.Windows.Forms.Application+ThreadContext)->
    19010b78(DemoApp.FormDemoApp)->
    19011158(System.Windows.Forms.PropertyStore)->
    … [omitted]
    1c3745ec(System.Data.DataTable)->
    1c3747a8(System.Data.DataColumnCollection)->
    1c3747f8(System.Collections.Hashtable)->
    1c376590(System.Collections.Hashtable+bucket[])->
    1c376c98(System.Data.DataColumn)->
    1c37b270(System.Data.Common.DoubleStorage)->
    1c37b2ac(System.Double[])
    Scan Thread 0 OSTHread 99c
    Scan Thread 6 OSTHread 484
    

    gcroot 命令可能需要很长时间才能完成。 任何未由垃圾回收功能回收掉的对象都是活动对象。 也就是说某些根直接或间接地驻留在对象上,所以 gcroot 应返回对象的路径信息。 您应该检查返回的关系图,以查看这些对象仍被引用的原因。

确定终结器是否已运行

  • 运行包含以下代码的测试程序:

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    

    如果测试解决了问题,则表明垃圾回收器未回收对象,因为这些对象的终结器已挂起。 GC.WaitForPendingFinalizers 方法允许终结器完成它们的任务,并修复问题。

确定是否存在等待终结的对象

  1. 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入以下命令:

    !finalizequeue

    查看准备终止的对象的数量。 如果数量大,则必须检查这些终结器根本没有进展或者进展速度慢的原因。

  2. 若要获得线程的输出,请键入以下命令:

    threads -special

    该命令提供如下输出。

           OSID     Special thread type
        2    cd0    DbgHelper 
        3    c18    Finalizer 
        4    df0    GC SuspendEE 
    

    终结器线程指示当前正在运行哪个终结器(如果有)。 当终结器线程未运行任何终结器时,表明它在等待某个事件告知它开始其工作。 大多数时候,您将看到终结器线程处于此状态,因为它在 THREAD_HIGHEST_PRIORITY 处运行,并应该很快结束运行终结器(如果有)。

确定托管堆中可用空间的大小

  • 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入以下命令:

    !dumpheap -type Free -stat

    该命令显示托管堆上所有可用对象的总大小,如下面的示例所示。

    total 230 objects
    Statistics:
          MT    Count    TotalSize Class Name
    00152b18      230     40958584      Free
    Total 230 objects
    
  • 若要确定第 0 代中的可用空间,请键入以下命令以获得代的内存使用信息:

    !eeheap -gc

    该命令显示类似下面的输出。 最后一行显示简短的段。

    Heap 0 (0015ad08)
    generation 0 starts at 0x49521f8c
    generation 1 starts at 0x494d7f64
    generation 2 starts at 0x007f0038
    ephemeral segment allocation context: none
    segment  begin     allocated  size
    00178250 7a80d84c  7a82f1cc   0x00021980(137600)
    00161918 78c50e40  78c7056c   0x0001f72c(128812)
    007f0000 007f0038  047eed28   0x03ffecf0(67103984)
    3a120000 3a120038  3a3e84f8   0x002c84c0(2917568)
    46120000 46120038  49e05d04   0x03ce5ccc(63855820)
    
  • 计算第 0 代使用的空间:

    ? 49e05d04-0x49521f8c

    结果如下所示。 第 0 代大约是 9 MB。

    Evaluate expression: 9321848 = 008e3d78
    
  • 下面的命令转储第 0 代范围内的可用空间:

    !dumpheap -type Free -stat 0x49521f8c 49e05d04

    结果如下所示。

    ------------------------------
    Heap 0
    total 409 objects
    ------------------------------
    Heap 1
    total 0 objects
    ------------------------------
    Heap 2
    total 0 objects
    ------------------------------
    Heap 3
    total 0 objects
    ------------------------------
    total 409 objects
    Statistics:
          MT    Count TotalSize Class Name
    0015a498      409   7296540      Free
    Total 409 objects
    

    该输出指示堆的第 0 代部分用于对象的空间为 9 MB,另外有 7 MB 的可用空间。 该分析指示第 0 代碎片化的程度。 应将该堆使用量作为长期对象所产生的碎片从总量中减去。

确定固定对象的数量

  • 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入以下命令:

    !gchandles

    显示的统计信息包括固定句柄的数量,如下面的示例所示。

    GC Handle Statistics:
    Strong Handles:      29
    Pinned Handles:      10
    

确定垃圾回收的时间

  • 检查 % Time in GC 内存性能计数器。

    该值使用采样间隔时间进行计算。 由于计数器在每次垃圾回收结束时进行更新,因此如果间隔期间没有发生回收,则当前采样与之前的采样将具有相同的值。

    通过将示例间隔时间乘以百分比值,可计算出回收时间。

    下面的数据显示了 4 个采样间隔(每个间隔 2 秒,总共 8 秒)以供学习研究。 Gen0、Gen1 和 Gen2 列显示该代间隔期间发生的垃圾回收的数量。

    Interval    Gen0    Gen1    Gen2    % Time in GC
           1       9       3       1              10
           2      10       3       1               1
           3      11       3       1               3
           4      11       3       1               3
    

    该信息不显示垃圾回收发生的时间,但是您可以确定间隔时间内发生的垃圾回收数量。 假设最糟糕的情况,第 0 代的第 10 次垃圾回收在第 2 个间隔开始时完成,而第 0 代的第 11 次垃圾回收在第 5 个间隔结束时完成。 从第 10 次垃圾回收结束到第 11 次垃圾回收结束的时间大约为 2 秒,并且性能计数器显示 3%,所以第 0 代的第 11 次垃圾回收的持续时间为(2 秒 * 3% = 60 毫秒)。

    在本示例中,有 5 个期间。

    Interval    Gen0    Gen1    Gen2     % Time in GC
           1       9       3       1                3
           2      10       3       1                1
           3      11       4       2                1
           4      11       4       2                1
           5      11       4       2               20
    

    第 2 代的第 2 次垃圾回收在第 3 个间隔期间开始,并结束于第 5 个间隔。 假设最糟糕的情况是,上一次垃圾回收是第 0 代垃圾回收,并于第 2 个间隔开始时完成,而第 2 代垃圾回收于第 5 个间隔结束时完成。 因此,第 0 代垃圾回收结束和第 2 代垃圾回收结束之间的时间为 4 秒。 因为 % Time in GC 计数器为 20%,所以第 2 代垃圾回收可能花费的最长时间为(4 秒 * 20% = 800 毫秒)。

  • 或者,您可以使用垃圾回收 ETW 事件确定垃圾回收的时长,并分析信息以确定垃圾回收的持续时间。

    例如,下面的数据显示了非并发垃圾回收期间发生的事件序列。

    Timestamp    Event name
    513052        GCSuspendEEBegin_V1
    513078        GCSuspendEEEnd
    513090        GCStart_V1
    517890        GCEnd_V1
    517894        GCHeapStats
    517897        GCRestartEEBegin
    517918        GCRestartEEEnd
    

    挂起托管的线程占用 26us (GCSuspendEEEnd – GCSuspendEEBegin_V1)。

    实际垃圾回收占用 4.8ms (GCEnd_V1 – GCStart_V1)。

    恢复托管的线程占用 21us (GCRestartEEEnd – GCRestartEEBegin)。

    下面的输出提供后台垃圾回收的示例,并包括进程、线程和事件字段。 (并未显示全部数据。)

    timestamp(us)    event name            process    thread    event field
    42504385        GCSuspendEEBegin_V1    Test.exe    4372             1
    42504648        GCSuspendEEEnd         Test.exe    4372        
    42504816        GCStart_V1             Test.exe    4372        102019
    42504907        GCStart_V1             Test.exe    4372        102020
    42514170        GCEnd_V1               Test.exe    4372        
    42514204        GCHeapStats            Test.exe    4372        102020
    42832052        GCRestartEEBegin       Test.exe    4372        
    42832136        GCRestartEEEnd         Test.exe    4372        
    63685394        GCSuspendEEBegin_V1    Test.exe    4744             6
    63686347        GCSuspendEEEnd         Test.exe    4744        
    63784294        GCRestartEEBegin       Test.exe    4744        
    63784407        GCRestartEEEnd         Test.exe    4744        
    89931423        GCEnd_V1               Test.exe    4372        102019
    89931464        GCHeapStats            Test.exe    4372        
    

    42504816 处的 GCStart_V1 事件指示其为后台垃圾回收,因为最后一个字段为 1。 这将形成垃圾回收编号 102019.

    发生 GCStart 事件的原因是,在启动后台垃圾回收之前需要进行一次短期的垃圾回收。 这将形成垃圾回收编号 102020.

    在 42514170 处,垃圾回收编号 102020 结束。 托管线程此时重新启动。 这在线程 4372 上完成,从而触发了此次后台垃圾回收。

    在线程 4744 上,发生挂起。 这是后台垃圾回收必须挂起托管线程的唯一时间。 该持续时间大约为 99ms ((63784407-63685394)/1000)。

    后台垃圾回收的 GCEnd 事件发生在 89931423。 这意味着后台垃圾回收持续了大约 47 秒 ((89931423-42504816)/1000)。

    托管线程运行的同时,您可以看到任意次数的短期垃圾回收发生。

确定触发垃圾回收的原因

  • 在加载了 SOS debugger extension 的 WinDbg 或 Visual Studio 调试器中,键入以下命令以显示所有线程及其调用堆栈:

    ~*kb

    该命令显示类似下面的输出。

    0012f3b0 79ff0bf8 mscorwks!WKS::GCHeap::GarbageCollect 
    0012f454 30002894 mscorwks!GCInterface::CollectGeneration+0xa4
    0012f490 79fa22bd fragment_ni!request.Main(System.String[])+0x48
    

    如果垃圾回收是由操作系统的内存不足通知引起,则调用堆栈也相似,只不过线程是终结器线程。 终结器线程收到异步内存不足通知,并引发垃圾回收。

    如果垃圾回收是由内存分配引起的,则堆栈显示如下:

    0012f230 7a07c551 mscorwks!WKS::GCHeap::GarbageCollectGeneration
    0012f2b8 7a07cba8 mscorwks!WKS::gc_heap::try_allocate_more_space+0x1a1
    0012f2d4 7a07cefb mscorwks!WKS::gc_heap::allocate_more_space+0x18
    0012f2f4 7a02a51b mscorwks!WKS::GCHeap::Alloc+0x4b
    0012f310 7a02ae4c mscorwks!Alloc+0x60
    0012f364 7a030e46 mscorwks!FastAllocatePrimitiveArray+0xbd
    0012f424 300027f4 mscorwks!JIT_NewArr1+0x148
    000af70f 3000299f fragment_ni!request..ctor(Int32, Single)+0x20c
    0000002a 79fa22bd fragment_ni!request.Main(System.String[])+0x153
    

    实时帮助器 (JIT_New*) 最后调用 GCHeap::GarbageCollectGeneration。 如果您确定第 2 代垃圾回收是由分配引起的,则必须确定第 2 代垃圾回收回收的是哪些对象以及如何避开它们。 即,您要确定第 2 代垃圾回收开始和结束时的区别,以及引发第 2 代回收的对象。

    例如,在调试器中键入以下命令可指示第 2 代回收开始:

    !dumpheap –stat

    示例输出(经过删减,以显示使用最多空间的对象):

    79124228    31857      9862328 System.Object[]
    035f0384    25668     11601936 Toolkit.TlkPosition
    00155f80    21248     12256296      Free
    79103b6c   297003     13068132 System.Threading.ReaderWriterLock
    7a747ad4   708732     14174640 System.Collections.Specialized.HybridDictionary
    7a747c78   786498     15729960 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700298     19608344 System.Collections.Specialized.ListDictionary
    035f0ee4    89192     38887712 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    7912c444    91616     71887080 System.Double[]
    791242ec    32451     82462728 System.Collections.Hashtable+bucket[]
    790fa3e0  2459154    112128436 System.String
    Total 6471774 objects
    

    在第 2 代结束时重复该命令:

    !dumpheap –stat

    示例输出(经过删减,以显示使用最多空间的对象):

    79124228    26648      9314256 System.Object[]
    035f0384    25668     11601936 Toolkit.TlkPosition
    79103b6c   296770     13057880 System.Threading.ReaderWriterLock
    7a747ad4   708730     14174600 System.Collections.Specialized.HybridDictionary
    7a747c78   786497     15729940 System.Collections.Specialized.ListDictionary+DictionaryNode
    7a747bac   700298     19608344 System.Collections.Specialized.ListDictionary
    00155f80    13806     34007212      Free
    035f0ee4    89187     38885532 Toolkit.TlkOrder
    00fcae40     6193     44911636 WaveBasedStrategy.Tick_Snap[]
    791242ec    32370     82359768 System.Collections.Hashtable+bucket[]
    790fa3e0  2440020    111341808 System.String
    Total 6417525 objects
    

    double[] 对象从输出的末尾消失,这意味着它们被回收。 这些对象大约占用 70 MB 的空间。 剩余的对象变动不大。 因此,这些 double[] 对象即为第 2 代垃圾回收发生的原因。 您的下一步是确定 double[] 对象存在和遭弃用的原因。 您可以询问代码开发人员这些对象的原位置,也可以使用 gcroot 命令。

确定高 CPU 使用率是否由垃圾回收引起

  • 综合考虑 % Time in GC 内存性能计数器值和进程时间。

    如果 % Time in GC 值高峰出现的时间与进程时间相同,则表示垃圾回收造成了高 CPU 使用率。 否则,请分析应用程序以查明高使用率发生的位置。

请参见

概念

垃圾回收