本文介绍与垃圾回收和内存使用情况相关的问题。 它解决了与托管堆相关的问题,并说明了如何最大程度地减少垃圾回收对应用程序的影响。 每个问题都有可用于调查问题的过程的链接。
性能分析工具
以下部分介绍可用于调查内存使用情况和垃圾回收问题的工具。 本文稍后提供的操作步骤与这些工具有关。
内存性能计数器
可以使用性能计数器来收集性能数据。 有关说明,请参阅 运行时分析。 性能计数器的 .NET CLR 内存类别,如 .NET 中的性能计数器中所述,提供有关垃圾回收器的信息。
使用 SOS 进行调试
可以使用 Windows 调试器(WinDbg) 检查托管堆上的对象。
若要安装 WinDbg,请从“ 适用于 Windows 的下载调试工具”页安装适用于 Windows 的调试工具 。
垃圾回收 ETW 事件
Windows 事件跟踪(ETW)是一个跟踪系统,用于补充 .NET 提供的分析和调试支持。 从 .NET Framework 4 开始, 垃圾回收 ETW 事件 从统计角度捕获用于分析托管堆的有用信息。 例如,在即将发生垃圾回收时引发的 GCStart_V1
事件提供以下信息:
- 正在收集哪一代对象。
- 垃圾回收的触发因素。
- 垃圾回收的类型(并发或非并发)。
ETW 事件日志记录效率高,不会屏蔽与垃圾回收相关的任何性能问题。 一个进程可以通过结合 ETW 事件来提供其自身的事件。 记录后,应用程序的事件和垃圾回收事件都可以关联,以确定堆问题发生的方式和时间。 例如,服务器应用程序可以在客户端请求的开头和结尾提供事件。
分析 API
公共语言运行时 (CLR) 分析接口提供有关垃圾回收期间受影响的对象的详细信息。 垃圾回收开始和结束时,可以通知探查器。 它可以提供有关托管堆上的对象的报表,包括每个代中的对象的标识。 有关详细信息,请参阅 分析概述。
探查器可以提供全面的信息。 但是,复杂的探查器可能会修改应用程序的行为。
应用程序域资源监视
从 .NET Framework 4 开始,应用程序域资源监视(ARM)使主机能够监视应用程序域的 CPU 和内存使用情况。 有关详细信息,请参阅 应用程序域资源监视。
诊断性能问题
第一步是 确定问题是否实际上是垃圾回收。 如果确定是,请从以下列表中选择以解决问题。
问题:抛出内存不足异常
对于引发的托管 OutOfMemoryException,存在以下两种合理的情况:
虚拟内存不足。
垃圾回收器按预先确定大小的分段来分配系统内存。 如果分配需要额外的段,但进程虚拟内存空间中没有连续的可用块,则托管堆的分配将失败。
没有足够的物理内存来分配。
性能检查 |
---|
确定内存不足异常是否受控。 确定可以保留多少虚拟内存。 确定是否有足够的物理内存。 |
如果确定异常不合法,请联系Microsoft客户服务和支持部门,并提供以下信息:
- 带有托管内存不足异常的堆栈。
- 完整内存转储。
- 证明这不是合法的内存不足异常的数据,包括显示虚拟或物理内存不是问题的数据。
问题:进程使用过多的内存
一个常见的假设是,内存使用情况显示在 Windows 任务管理器 的“性能 ”选项卡上,可以指示何时使用了过多的内存。 但是,该显示与工作集相关;它不提供有关虚拟内存使用情况的信息。
如果确定问题是托管堆引发的,必须测量一段时间的托管堆,以确定模式。
如果确定问题不是托管堆引发的,则必须使用本地调试。
性能检查 |
---|
确定可以保留多少虚拟内存。 确定托管堆的内存提交量。 确定托管堆保留多少内存。 确定第 2 代中的大型对象。 确定对对象的引用。 |
问题:垃圾回收器无法快速回收对象
当出现对象好像未按垃圾回收的预期进行回收的情况时,必须确定是否存在任何对这些对象的强引用。
如果没有对包含死对象的一代进行垃圾回收,这表示尚未运行死对象的终结器,你也可能会遇到以上问题。 例如,当正在运行一个单线程单元 (STA) 应用程序并且服务终结器队列的线程不能调用至其中时,可能发生这种问题。
性能检查 |
---|
检查对对象的引用。 确定是否已运行终结器。 确定是否存在正在等待完成的对象。 |
问题:托管堆太零碎
碎片级别将计算为可用空间占这一代已分配的总内存的比率。 对于第 2 代,可接受的碎片化水平不超过 20%。 由于第 2 代可以变得非常大,碎片的比例比绝对值更重要。
第 0 代中具有大量可用空间并不是问题,因为这是分配新对象的代系。
大型对象堆中始终会产生碎片,因为它没有经过压缩。 相邻的自由对象自然会折叠成单个空间以满足大型对象分配请求。
在第 1 代和第 2 代中,碎片可能会成为问题。 如果这些代在垃圾回收后具有大量可用空间,则应用程序的对象使用情况可能需要修改,并且应考虑重新评估长期对象的生存期。
固定对象过多可能会增加碎片。 如果碎片太多,则可以固定许多对象。
如果虚拟内存碎片阻止垃圾回收器添加段,原因可能是以下原因之一:
频繁加载和卸载许多小的程序集。
与非托管代码互操作时,保留了太多对 COM 对象的引用。
创建大型暂时性对象,这会导致大型对象堆频繁分配和释放堆段。
托管 CLR 时,应用程序可以请求垃圾回收器保留其段。 这将减少段分配的频率。 这是通过使用 STARTUP_FLAGS枚举中的STARTUP_HOARD_GC_VM标志来实现的。
性能检查 |
---|
确定托管堆中的可用空间量。 确定固定对象的数量。 |
如果您认为没有合法理由导致碎片化,请联系微软客户服务与支持。
问题:垃圾回收暂停时间过长
垃圾回收以软实时方式运行,因此应用程序必须能够容忍某些暂停。 软实时的一个衡量标准是 95% 的操作必须按时完成。
在并发垃圾回收过程中,允许托管线程在回收期间运行,这意味着暂停时间非常短。
临时垃圾回收(第 0 代和第 1 代)仅持续几毫秒,因此减少暂停通常不可行。 然而,你可以通过更改应用程序的分配请求的模式,在第 2 代回收中减少暂停。
另一种更准确的方法是使用 垃圾回收 ETW 事件。 可以通过为某个事件序列添加时间戳的差异来查找回收的计时。 整个集合序列包括暂停执行引擎、垃圾回收本身以及恢复执行引擎。
可以使用 垃圾回收通知 来确定服务器是否即将进行第 2 代回收,以及是否将请求重新路由到另一台服务器可能会缓解暂停任何问题。
性能检查 |
---|
确定垃圾回收中的时间长度。 确定导致垃圾回收的原因。 |
问题:第 0 代太大
第 0 代在 64 位系统上可能拥有更多的对象,尤其是在使用服务器垃圾回收而非工作站垃圾回收时。 这是因为在这些环境中触发第 0 代垃圾回收的阈值更高,第 0 代回收可能会变得更大。 当应用程序在触发垃圾回收之前分配更多内存时,性能会得到改善。
问题:垃圾回收期间的 CPU 使用率过高
在垃圾回收期间,CPU 使用率会很高。 如果在垃圾回收中花费了大量进程时间,则回收次数太频繁,或者回收时间过长。 托管堆上增加的对象分配率将导致垃圾回收更频繁地发生。 降低分配率可降低垃圾回收的频率。
可以使用性能计数器监视分配速率 Allocated Bytes/second
。 有关详细信息,请参阅 .NET 中的性能计数器。
集合的持续时间主要取决于分配后存活的对象数量。 如果还有许多对象需要被回收,垃圾回收器必须遍历大量内存。 整理幸存者的工作非常耗时。 若要确定回收期间处理对象的数量,请在指定代的垃圾回收结束时,在调试器中设置一个断点。
性能检查 |
---|
确定垃圾回收是否导致 CPU 使用率过高。 在垃圾回收结束时设置断点。 |
故障排除指南
本部分介绍在开始调查时应考虑的准则。
工作站或服务器垃圾回收
确定是否使用正确的垃圾回收类型。 如果应用程序使用多个线程和对象实例,请使用服务器垃圾回收而不是工作站垃圾回收。 服务器垃圾回收在多个线程上运行,而工作站垃圾回收需要应用程序的多个实例来运行自己的垃圾回收线程并争用 CPU 时间。
具有低负载且不经常在后台执行任务(例如服务)的应用程序可以使用工作站垃圾回收并禁用并发垃圾回收。
何时衡量托管堆的大小
除非使用探查器,否则必须建立一致的度量模式来有效诊断性能问题。 请考虑以下几点来建立计划:
- 如果在第 2 代垃圾回收后测量,则整个托管的堆将不再存在垃圾(死对象)。
- 如果在第 0 代垃圾回收后立即测量,第 1 代和第 2 代的对象将尚未被回收。
- 如果在垃圾回收之前立即进行测量,则你将在垃圾回收启动之前,测量尽可能多的分配。
- 在垃圾回收期间进行测量是有问题的,因为垃圾回收器数据结构没有遍历的有效状态,可能无法提供完整的结果。 这是设计造成的。
- 将工作站垃圾回收与并发垃圾回收结合使用时,回收的对象不会压缩,因此堆大小可以相同或更大(碎片可能使其看起来更大)。
- 当物理内存负载过高时,第 2 代上的并发垃圾回收将延迟。
以下过程介绍如何设置一个断点,以便测量托管堆。
若要在垃圾回收结束时设置一个断点
在加载 SOS 调试器扩展的 WinDbg 中,输入以下命令:
bp mscorwks!WKS::GCHeap::RestartEE "j (dwo(mscorwks!WKS::GCHeap::GcCondemnedGeneration)==2) 'kb';'g'"
将
GcCondemnedGeneration
设置为所需的代。 此命令需要专用符号。如果在已回收第 2 代对象以进行垃圾回收后执行
RestartEE
,则此命令会强制中断。在服务器垃圾回收中,只有一个线程调用
RestartEE
,因此断点仅在第 2 代垃圾回收期间发生一次。
性能检查过程
本部分介绍以下过程来隔离性能问题的原因:
- 确定问题是否由垃圾回收引起。
- 确定内存不足异常是否受控。
- 确定可以保留多少虚拟内存。
- 确定是否有足够的物理内存。
- 确定托管堆的内存提交量。
- 确定托管堆保留多少内存。
- 确定第 2 代中的大型对象。
- 确定对对象的引用。
- 确定是否已运行终结器。
- 确定是否存在正在等待完成的对象。
- 确定托管堆中的可用空间量。
- 确定固定对象的数量。
- 确定垃圾回收中的时间长度。
- 确定触发垃圾回收的内容。
- 确定高 CPU 使用率是否由垃圾回收引起。
确定问题是否由垃圾回收引起
检查以下两个内存性能计数器:
GC 所占时间百分比。 显示上次垃圾回收周期后执行垃圾回收所用的已用时间百分比。 使用此计数器确定垃圾回收器是否花费太多时间来使托管堆空间可用。 如果垃圾回收所用的时间相对较短,这可能表示托管堆之外存在资源问题。 当涉及并发或后台垃圾回收时,此计数器可能不准确。
# 已提交的字节总数。 显示垃圾回收器当前已提交的虚拟内存量。 使用此计数器来确定垃圾回收器使用的内存是否是应用程序使用的内存的过多部分。
大多数内存性能计数器会在每次垃圾回收结束时更新。 因此,它们可能不会反映所需信息的当前条件。
若要确定是否已托管内存不足异常
在加载 SOS 调试器扩展的 WinDbg 或 Visual Studio 调试器中,输入打印异常 (
pe
) 命令:!pe
如果异常是托管的, OutOfMemoryException 则显示为异常类型,如以下示例所示。
Exception object: 39594518 Exception type: System.OutOfMemoryException Message: <none> InnerException: <none> StackTrace (generated):
如果输出未指定异常,则必须确定内存不足异常来自哪个线程。 在调试器中输入以下命令,以显示所有线程及其调用堆栈:
~\*kb
具有存在异常调用的堆栈的线程会由
RaiseTheException
参数进行指示。 这是托管异常对象。28adfb44 7923918f 5b61f2b4 00000000 5b61f2b4 mscorwks!RaiseTheException+0xa0
你可以使用以下命令来导出嵌套异常。
!pe -nested
如果找不到任何异常,则内存不足异常源自非托管代码。
确定可以保留多少虚拟内存
在加载 SOS 调试器扩展的 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
确定是否有足够的物理内存
启动 Windows 任务管理器。
在
Performance
选项卡上,查看已提交的值。 (在 Windows 7 中,在Commit (KB)
中查看System group
。)如果
Total
接近Limit
,则表明物理内存不足。
若要确定托管堆的内存提交量
使用
# Total committed bytes
内存性能计数器获取托管堆提交的字节数。 垃圾回收器根据需要在某个段上提交区块,但不会全部在同一时间进行。注释
请勿使用
# Bytes in all Heaps
性能计数器,因为它不表示托管堆的实际内存使用量。 代的大小包括在此值中,且实际上是其阈值大小,即如果代以对象进行填充,将引发垃圾回收的大小。 因此,此值通常为零。
若要确定托管堆的内存保留量
使用
# Total reserved bytes
内存性能计数器。垃圾回收器在段中保留内存,你可以确定段从何处开始使用
eeheap
命令。重要
虽然可以确定垃圾回收器为每个段分配的内存量,但段大小是特定于实现的,并且随时可能会更改,包括定期更新。 应用程序不应假设特定段的大小或依赖于此大小,也不应尝试配置段分配可用的内存量。
在加载 SOS 调试器扩展的 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 调试器扩展的 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 调试器扩展的 WinDbg 中,输入以下命令列出对对象的引用:
!gcroot
或
若要确定特定对象的引用,请包括地址:
!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 方法将启用这些终结器来完成其任务,并解决问题。
确定是否存在正在等待完成的对象
在加载 SOS 调试器扩展的 WinDbg 或 Visual Studio 调试器中,输入以下命令:
!finalizequeue
查看已准备好进行终结的对象的数目。 如果数目很多,则必须检查这些终结器完全没有进展或进展速度不够快的原因。
若要获取线程的输出,请输入以下命令:
!threads -special
此命令提供如下输出。
OSID Special thread type 2 cd0 DbgHelper 3 c18 Finalizer 4 df0 GC SuspendEE
终结器线程指示当前正在运行哪个终结器(如果有)。 当终结器线程没有运行任何终结器时,则它正在等待一个事件告诉它进行工作。 大多数情况下,你将看到此状态中的终结器线程,因为它在 THREAD_HIGHEST_PRIORITY 处运行,并应快速完成运行终结器(如果存在)。
若要确定托管堆中的可用空间量
在加载 SOS 调试器扩展的 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 调试器扩展的 WinDbg 或 Visual Studio 调试器中,输入以下命令:
!gchandles
显示的统计信息包括固定句柄的数量,如以下示例所示。
GC Handle Statistics: Strong Handles: 29 Pinned Handles: 10
确定垃圾回收过程的持续时间
检查
% Time in GC
内存性能计数器。该值是使用样本间隔时间计算的。 由于计数器在每个垃圾回收结束时更新,因此,如果间隔内未发生任何回收,则当前示例将具有与上一个示例相同的值。
通过将样本间隔时间与百分比值相乘来获取收集时间。
以下数据显示了为时 8 秒的研究的 4 个采样,彼此间隔 2 秒。
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 垃圾回收在第二个间隔开始时完成,第十一代 0 垃圾回收在第三个间隔结束时完成。 第十一代垃圾回收结束到第十一次垃圾回收结束之间的时间约为 2 秒,性能计数器显示 3%,因此第十一代 0 垃圾回收的持续时间为 (2 秒 * 3% = 60 毫秒)。
在下一个示例中,有五个间隔。
Interval Gen0 Gen1 Gen2 % Time in GC 1 9 3 1 3 2 10 3 1 1 3 11 4 1 1 4 11 4 1 1 5 11 4 2 20
第二代垃圾回收在第四个时间间隔内开始,在第五个间隔完成。 假设情况最差,最后一次垃圾回收是第 0 代回收在第三个间隔开始时完成,第 2 代垃圾回收在第五个间隔结束时完成。 因此,第 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.8 毫秒(
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
。 这将变为垃圾回收 No.102019。GCStart
事件发生是因为在启动后台垃圾回收之前,需要进行一次短暂的垃圾回收。 这将变为垃圾回收 No. 102020。在 42514170 处,垃圾回收 No.102020 结束。 此时会重启托管线程。 此操作在线程 4372 上完成,该线程触发了此后台垃圾回收。
在线程 4744 上,发生了一个挂起。 这是唯一一次后台垃圾回收不得不挂起托管线程。 此持续时间约为 99 毫秒(63784407-63685394)/1000。
后台垃圾回收的
GCEnd
事件位于 89931423。 这意味着后台垃圾回收持续了大约 47 秒(89931423-42504816)/1000。托管线程运行时,可以查看发生的任意数量的暂时垃圾回收。
确定导致垃圾回收的原因
在加载 SOS 调试器扩展的 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 使用率过高。 否则,请分析应用程序,以查找出现高使用率的位置。