研究托管内存问题时涉及到的工具, 计数器, 和WinDBG命令
Window 任务管理器
==============
Mem Usage 表示的是进程工作集(就像进程\工作集性能计数器)。它并不表示所使用的字节数(committed bytes)。
VM Size 反映的是供进程使用的字节数(就像进程\专用字节数性能计数器)。VM Size 可提供关于您是否面临内存泄漏问题的第一线索(如果您的应用程序存在泄漏,则 VM Size 会随时间增加).
GC 性能计数器
==============
每项研究的第一步是收集相关数据并对可能存在问题的位置做出假设。通常首先从性能计数器开始。
请注意,.NET 内存性能计数器只有在收集时才更新,而不是根据性能监视器应用程序中使用的采样率进行更新。
- 应该首先检查 % Time in GC. 它表示自从上次收集结束后花在 GC 上的时间的百分比. 如果您发现此数值非常高(假设为 50% 或更高),那么您应该检查一下托管堆内部发生了哪些情况。如果 % Time in GC 没有超过 10%,那么通常就不必花时间来尝试减少 GC 用在收集上的时间了,因为这样做带来的益处微乎其微。
- 如果您认为您的应用程序在执行垃圾收集上花费的时间过多,那么下一个要检查的性能计数器就是 Allocated Bytes/sec(每秒分配字节数)。该计数器显示了分配速率。不过,该计数器在分配速率非常低的情况下,并不十分准确。如果采样频率高于收集频率,该计数器可能显示为 0 字节/秒,因为该计数器只有在每次收集开始的时候进行更新。这并不意味着没有进行分配操作,只是由于在该时间间隔内没有收集发生,因此计数器没有得到更新而已。
- 如果您认为您要收集大量大型对象(85,000 字节或更大),则需要检查大型对象堆Large Object Heap (LOH) 的大小。它与 Allocated Bytes/sec 同时更新。
- 高分配速率会导致大量收集工作,因此可能 % Time in GC 会比较高。能否减轻这一现象的一个因素是看对象通常是否能很早就死去,只因为它们通常会在第 0 级收集过程中被收集。要确定对象生命周期对收集有何影响,可检查各级收集的性能计数器:# Gen 0 Collections(第 0 级收集次数)、# Gen 1 Collections(第 1 级收集次数)、# Gen 2 Collections(第 2 级收集次数)。这些性能计数器显示自进程启动后对各级对象进行收集的次数。第 0 级和第 1 级收集通常开销很低,因此它们不会对应用程序的性能有很大影响。而第 2 级收集器开销非常大。
首要原则是,各级收集之间合理的比值是每进行 10 次第 1 级收集,进行一次第 2 级收集。如果您发现在垃圾收集上花费了大量时间,那可能是由于第 2 级收集的频率过高造成的。您应该检查上面提到的比值,确保第 2 级收集与第 1 级收集的次数比值不是太高。 - 您可能会发现 % Time in GC 很高,但分配速率并不高。如果您分配的许多对象能够在垃圾收集后保留下来并被提升到下一级,则会出现这种情况。提升计数器 — 从第 0 级提升的内存 (Promoted Memory from Gen 0) 和从第 1 级提升的内存 (Promoted Memory from Gen 1) — 可以告诉您提升速率是否存在问题。我们希望避免从第 1 级提升的速率太高。这是因为您可能有大量对象存在时间较长,足以提升到第 2 级,但存在的时间不足以使其保留在第 2 级中。一旦提升到第 2 级,这些对象的收集开销就要比它们在第 1 级中死去要大。(这种现象被称为中年危机。有关详细信息,请参阅 blogs.msdn.com/41281.aspx。)CLR 分析器 (CLR Profiler) 可帮您了解哪些对象存在时间过长。
- 第 1 级和第 2 级堆大小的数值较高往往与提升速率计数器中的数值较高相关。您可以使用第 1 级堆大小(Gen 1 heap size)和第 2 级堆大小(Gen 1 heap size)来检查 GC 堆的大小。有一个第 0 级堆大小计数器,但它并不用于衡量第 0 级的大小。它用于表示第 0 级的空间预算 — 意味着在触发下一次第 0 级收集之前,在第 0 级中您可以分配的字节数。
- 如果您使用了的大量需要终结的对象 — 例如,依赖于 COM 组件进行一些处理的对象 — 在这种情形下,您可以看一下 Promoted Finalization-Memory from Gen 0(从第 0 级提升的终结内存)计数器。该计数器会告诉您由于使用内存的对象需要被添加到终结队列中而无法立即对其进行收集、由此导致无法被重复使用的内存数量。 IDisposable 和 C# 及 Visual Basic® 中的 using 语句可帮助减少在终结队列中结束的对象数量,从而降低相关的开销。
- 使用 # Total committed Bytes(提供的字节总数)和 # Total reserved Bytes(保留的字节总数)可找到关于堆大小的详细数据。这些计数器分别表示当前在 GC 堆上提供内存和保留内存的总数。(提供的字节总数值略微大于实际的第 0 级堆大小 + 第 1 级堆大小 + 第 2 级堆大小 + 大型对象堆大小。)当 GC 分配一个新堆段时,内存将保留给该段,只有在需要时才提供内存。因此保留字节的总数可以比提供的字节总数大。
- 同样应该检查一下应用程序是否引发了太多次收集。# Induced GC(引发的 GC 的数目)计数器可以告诉您自进程启动以来引发了多少次收集。一般而言,不建议您引发多次 GC 收集。在大多数情况下,如果 # Induced GC 的数值较高,您应该将其视为 Bug。在大多数情况下人们引发 GC 是希望削减堆的大小,但这并非理想的选择。您应该了解您的堆大小为何增加。
Windows 性能计数器
================
到目前为止,我们已经了解了一些最实用的 .NET 内存计数器。但您不应忽略其他计数器的价值。有很多种 Windows 性能计数器(也可通过 perfmon.exe 查看)为研究内存问题提供了有用的信息。
- Memory(内存)类别下面所列的 Available Bytes(可用字节)计数器报告了可用的物理内存。它可明确地显示您的物理内存是否过低。如果机器的物理内存过低,会发生分页或者很快会发生分页。该数据对于诊断 OOM 问题非常有用。
- % Committed Bytes in Use(正在使用的字节百分比)计数器(同样位于 Memory 类别下)提供了内存使用量与内存总量的比值。如果此值非常高(假设超过 90%),您应该开始检查提供内存故障。这明显表明系统内存紧张。
- Process(进程)类别下的 Private Bytes(专用字节数)计数器表示被使用且无法与其他进程共享的内存数量。如果您希望了解您的进程使用了多少内存,您应该监视此计数器。如果您遇到了内存泄漏问题,专用字节数会随时间增加。该计数器还可明显地表明了您的应用程序对整个系统的影响 — 使用大量专用字节会对机器有很大影响,因为内存无法与其他进程共享。这在某些情形下至关重要,如终端服务,在这种情形下您需要使用户会话之间共享的内存量达到最大。
WinDBG命令
===============
命令 | 功能 | 补充说明 |
!pe | 输出线程上的最后的托管异常(如果有的话) | !PrintException 的缩写形式 |
~*kb | 列出所有线程及其堆栈的调用 | Display Stack Backtrace(显示堆栈回溯)的缩写. 在结果中查找mscorwks::RaiseTheException来定位存在异常调用的线程和堆栈. |
!threads | 显示所有线程的状态 | |
!address | 显示虚拟内存的最大可用区域 | |
!eeheap –gc | 显示每个垃圾收集段的起始位置 | |
bp mscorwks!WKS::GCHeap::RestartEE "j(dwo(mscorwks!WKS::GCHeap::GcCondemnedGeneration)==2)'kb';'g'" | 在第 2 代垃圾收集结束时停止 | |
!dumpheap –stat | 对托管堆上的对象进行完整的转储 | |
!dumpheap -type System.String -min 150 -max 200 | 检查大小在 150 至 200 之间的所有字符串 | |
!gcroot | 显示某个地址上的对象的引用情况 | 帮助我们了解对象为何处于活动状态 |
!finalizequeue | 查看准备终结的所有对象 | 查看准备终结的对象数 — 而非“可终结对象数” |
!threads-special | 输出所有由CLR创建的特殊线程 | 特殊线程包括: 1. 垃圾收集线程. 2. debugger辅助线程 3. finalizer线程 4. 卸载Appdomain线程 5. 线程池timer线程. |
!objsize | 显示对象的大小 了解哪些对象被钉住 | 如果不带参数, 则显示 1. 所有托管线程上的对象的大小 2. 进程中所有垃圾收集器的句柄handle, 以及被这些句柄指向的对象的大小. 该命令除了显示父对象的大小之外, 还显示子对象的大小. 4. |
!gchandles | 显示进程中垃圾收集器句柄的统计信息 查看钉住句柄的数量 | 使用这个命令可以定位有垃圾收集器句柄泄露而引起的内存泄露问题. |
参考资料:
CLR完全介绍 - 研究内存问题