如何在windbg中分析未分类的内存使用情况

23

这是一款在x64机器上运行的.NET v4 Windows服务应用程序。在稳定运行数天后,Windows服务内存消耗量突然飙升,直到崩溃。我能够捕捉到它峰值为1.2 GB并且生成了一个内存转储文件。以下是我的处理结果:

如果我在我的转储文件中在Windbg中运行!address -summary,我会得到以下结果:

!address -summary

--- Usage Summary ------ RgnCount ------- Total Size -------- %ofBusy  %ofTotal
Free                     821      7ff`7e834000 (   7.998 Tb)           99.98%
<unclassified>           3696       0`6eece000 (   1.733 Gb)  85.67%   0.02%
Image                    1851       0`0ea6f000 ( 234.434 Mb)  11.32%   0.00%
Stack                    1881       0`03968000 (  57.406 Mb)  2.77%    0.00%
TEB                      628        0`004e8000 (   4.906 Mb)  0.24%    0.00%
NlsTables                1          0`00023000 ( 140.000 kb)  0.01%    0.00%
ActivationContextData    3          0`00006000 (  24.000 kb)  0.00%    0.00%
CsrSharedMemory          1          0`00005000 (  20.000 kb)  0.00%    0.00%
PEB                      1          0`00001000 (   4.000 kb)  0.00%    0.00%
-
-
-
--- Type Summary (for busy) -- RgnCount ----- Total Size ----- %ofBusy %ofTotal
MEM_PRIVATE                        5837 0`7115a000 (  1.767 Gb)  87.34%  0.02%
MEM_IMAGE                          2185 0`0f131000 (241.191 Mb)  11.64%  0.00%
MEM_MAPPED                           40 0`01531000 ( 21.191 Mb)   1.02%  0.00%
-
-
--- State Summary ------------ RgnCount ------ Total Size ---- %ofBusy %ofTotal
MEM_FREE                            821 7ff`7e834000 (  7.998 Tb)        99.98%
MEM_COMMIT                         6127   0`4fd5e000 (  1.247 Gb) 61.66%  0.02%
MEM_RESERVE                        1935   0`31a5e000 (794.367 Mb) 38.34%  0.01%
-
-
--Protect Summary(for commit)- RgnCount ------ Total Size --- %ofBusy %ofTotal
PAGE_READWRITE                     3412 0`3e862000 (1000.383 Mb) 48.29%   0.01%
PAGE_EXECUTE_READ                   220 0`0b12f000 ( 177.184 Mb)  8.55%   0.00%
PAGE_READONLY                       646 0`02fd0000 (  47.813 Mb)  2.31%   0.00%
PAGE_WRITECOPY                      410 0`01781000 (  23.504 Mb)  1.13%   0.00%
PAGE_READWRITE|PAGE_GUARD          1224 0`012f2000 (  18.945 Mb)  0.91%   0.00%
PAGE_EXECUTE_READWRITE              144 0`007b9000 (   7.723 Mb)  0.37%   0.00%
PAGE_EXECUTE_WRITECOPY               70 0`001cd000 (   1.801 Mb)  0.09%   0.00%
PAGE_EXECUTE                          1 0`00004000 (  16.000 kb)  0.00%   0.00%
-
-
--- Largest Region by Usage ----Base Address -------- Region Size ----------
Free                            0`8fff0000        7fe`59050000 (   7.994 Tb)
<unclassified>                  0`80d92000        0`0f25e000 ( 242.367 Mb)
Image                           fe`f6255000       0`0125a000 (  18.352 Mb)
Stack                           0`014d0000        0`000fc000 (1008.000 kb)
TEB                             0`7ffde000        0`00002000 (   8.000 kb)
NlsTables                       7ff`fffb0000      0`00023000 ( 140.000 kb)
ActivationContextData           0`00030000        0`00004000 (  16.000 kb)
CsrSharedMemory                 0`7efe0000        0`00005000 (  20.000 kb)
PEB                             7ff`fffdd000      0`00001000 (   4.000 kb)
首先,为什么未分类显示一次为1.73 GB,另一次为242 MB。(这个问题已经得到解答。谢谢) 其次,我了解到未分类可以意味着托管代码,但是根据!eeheap的结果,我的堆大小仅为248 MB,实际上与242相匹配,但远远达不到1.73GB。转储文件的大小为1.2 GB,比正常情况高得多。我应该从哪里开始查找使用所有内存的原因。托管堆世界中的任何内容都在248 MB以下,但我正在使用1.2 GB。
谢谢
编辑:
如果我执行!heap -s,则会得到以下结果。
LFH Key                   : 0x000000171fab7f20
Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
Virtual block: 00000000017e0000 - 00000000017e0000 (size 0000000000000000)
Virtual block: 0000000045bd0000 - 0000000045bd0000 (size 0000000000000000)
Virtual block: 000000006fff0000 - 000000006fff0000 (size 0000000000000000)
0000000000060000 00000002  113024 102028 113024  27343  1542    11    3    1c LFH
    External fragmentation  26 % (1542 free blocks)
0000000000010000 00008000      64      4     64      1     1     1    0    0      
0000000000480000 00001002    3136   1380   3136     20     8     3    0    0  LFH
0000000000640000 00041002     512      8    512      3     1     1    0    0      
0000000000800000 00001002    3136   1412   3136     15     7     3    0    0  LFH
00000000009d0000 00001002    3136   1380   3136     19     7     3    0    0  LFH
00000000008a0000 00041002     512     16    512      3     1     1    0    0      
0000000000630000 00001002    7232   3628   7232     18    53     4    0    0  LFH
0000000000da0000 00041002    1536    856   1536      1     1     2    0    0  LFH
0000000000ef0000 00041002    1536    944   1536      4    12     2    0    0  LFH
00000000034b0000 00001002    1536   1452   1536      6    17     2    0    0  LFH
00000000019c0000 00001002    3136   1396   3136     16     6     3    0    0  LFH
0000000003be0000 00001002    1536   1072   1536      5     7     2    0    3  LFH
0000000003dc0000 00011002     512    220    512    100    60     1    0    2      
0000000002520000 00001002     512      8    512      3     2     1    0    0      
0000000003b60000 00001002  339712 168996 339712 151494   976   116    0   18  LFH
    External fragmentation  89 % (976 free blocks)
    Virtual address fragmentation  50 % (116 uncommited ranges)
0000000003f20000 00001002      64      8     64      3     1     1    0      0      
0000000003d90000 00001002      64      8     64      3     1     1    0      0      
0000000003ee0000 00001002      64     16     64     11     1     1    0      0      
-------------------------------------------------------------------------------------

我的堆大小根据!eeheap(sos.dll)仅为248 MB。因此,我不确定这是1.2 GB进程大小的原因,也不确定未分类的1.7GB的原因,除非我漏掉了什么。 - Mark
你的服务是做什么的?你的服务包含未托管或C++/CLI代码吗?看起来像是未托管的内存泄漏。GDI、用户对象、句柄计数器显示了什么?你的线程卡在哪些调用堆栈中? ~*e!ClrStack 和 ~*e!DumpStack 和 ~*ekv 是查看线程正在做什么的好工具。有一个线程正在分配某些东西吗? - Alois Kraus
600个线程?CLR默认使用1 MB的堆栈空间,这是默认驻留的。在这种情况下,您已经使用了600 MB的内存仅用于线程堆栈。但是您的内存转储仅显示 57,406,大约为58个线程。您的最大线程堆栈正好为1 MB,这可能表明发生了堆栈溢出。WMI像疯子一样使用COM。很可能您会经常使用某种“WITHIN 0.1”形式的WMI查询,这将产生大量垃圾COM对象。 - Alois Kraus
@AloisKraus!threads 命令显示我有605个线程计数,其中599个是后台线程,5个是死线程。你是说由于堆栈大小只有57MB,而我的最大堆栈只有1MB,这证明了它是一种堆栈溢出吗?如果是这样的话,那将非常非常有用! - Mark
@AloisKraus,我已经检查了,并没有发现堆栈溢出异常。 - Mark
显示剩余5条评论
5个回答

17

我最近遇到了一个非常相似的情况,并发现在调查中有一些有用的技巧。其中没有一种是万无一失的,但每一种都可以为问题提供更多的解释。

1)来自SysInternals的vmmap.exe(http://technet.microsoft.com/en-us/sysinternals/dd535533)在相关本机和托管内存信息并将其以良好的UI呈现方面做得很好。可以使用下面的技术收集相同的信息,但这样更容易且是一个不错的起点。不幸的是,它不适用于转储文件,您需要一个活动进程。

2)"!address-summary"输出是更详细的"!address"输出的汇总。我发现把详细的输出放入Excel中并运行一些数据透视表很有用。使用这种技术,我发现许多字节被列为空字符串"",实际上是MEM_IMAGE页面,可能是当DLL加载时加载的数据页面的副本,但在数据更改时被复制了。我还可以过滤大区域并深入研究特定地址。用牙签和祈祷在内存转储中四处探索是很痛苦的,但也可能会有所收获。

3)最后,我使用了vmmap.exe技术的简单版本。我加载了转储文件,打开了日志,并运行了!address、!eeheap、!heap和!threads。我还针对~*k中列出的线程环境块使用!teb。我关闭了日志文件并在我喜欢的编辑器中加载它。然后我可以找到未分类的块并搜索以查看它是否在更详细的命令的输出中出现。您可以相当迅速地将本机和托管堆关联起来,以将其从可疑的未分类区域中排除。

所有这些都太手动化了。我希望编写一个脚本,可以接收类似于我在技巧3中生成的输出,并输出适合查看vmmap.exe的mmp文件。有一天。

最后需要注意的是,我对vmmap.exe的输出结果与!address的输出结果进行了相关性分析,并注意到vmmap能够从各种来源识别出这些类型的区域(与!heap和!eeheap使用的方式类似),但!address不知道这些内容。也就是说,这些是vmmap.exe标记但!address没有标记的东西:

.data
.pdata
.rdata
.text
64-bit thread stack
Domain 1
Domain 1 High Frequency Heap
Domain 1 JIT Code Heap
Domain 1 Low Frequency Heap
Domain 1 Virtual Call Stub
Domain 1 Virtual Call Stub Lookup Heap
Domain 1 Virtual Call Stub Resolve Heap
GC
Large Object Heap
Native heaps
Thread Environment Blocks

仍有很多未计算的"私有"字节,但如果我能排除这些,我就能缩小问题范围。

希望这些内容能给你一些调查思路。我也面临同样的问题,所以我很感谢你找到的答案。谢谢!


“我很想写一个脚本...用于查看vmmap”,你有机会这样做吗? - Thomas Weller

2
我保存了Debugging Tools for Windows 6.11.1.404的一个副本,它似乎能够显示更有意义的“未分类”内容。
使用该版本,我可以看到一个TEB地址列表,然后是这个:
0:000> !address -summary
 --------- PEB fffde000 not found ----
 TEB fffdd000 in range fffdb000 fffde000
 TEB fffda000 in range fffd8000 fffdb000
...snip...
 TEB fe01c000 in range fe01a000 fe01d000
 ProcessParametrs 002c15e0 in range 002c0000 003c0000
 Environment 002c0810 in range 002c0000 003c0000
-------------------- Usage SUMMARY --------------------------
    TotSize (      KB)   Pct(Tots) Pct(Busy)   Usage
   41f08000 ( 1080352) : 25.76%    34.88%    : RegionUsageIsVAD
   42ecf000 ( 1096508) : 26.14%    00.00%    : RegionUsageFree
    5c21000 (   94340) : 02.25%    03.05%    : RegionUsageImage
    c900000 (  205824) : 04.91%    06.64%    : RegionUsageStack
          0 (       0) : 00.00%    00.00%    : RegionUsageTeb
   68cf8000 ( 1717216) : 40.94%    55.43%    : RegionUsageHeap
          0 (       0) : 00.00%    00.00%    : RegionUsagePageHeap
          0 (       0) : 00.00%    00.00%    : RegionUsagePeb
          0 (       0) : 00.00%    00.00%    : RegionUsageProcessParametrs
          0 (       0) : 00.00%    00.00%    : RegionUsageEnvironmentBlock
       Tot: ffff0000 (4194240 KB) Busy: bd121000 (3097732 KB)

-------------------- Type SUMMARY --------------------------
    TotSize (      KB)   Pct(Tots)  Usage
   42ecf000 ( 1096508) : 26.14%   : <free>
    5e6e000 (   96696) : 02.31%   : MEM_IMAGE
    28ed000 (   41908) : 01.00%   : MEM_MAPPED
   b49c6000 ( 2959128) : 70.55%   : MEM_PRIVATE

-------------------- State SUMMARY --------------------------
    TotSize (      KB)   Pct(Tots)  Usage
   9b4d1000 ( 2544452) : 60.67%   : MEM_COMMIT
   42ecf000 ( 1096508) : 26.14%   : MEM_FREE
   21c50000 (  553280) : 13.19%   : MEM_RESERVE

Largest free region: Base bc480000 - Size 38e10000 (931904 KB)

使用我的“当前”版本(6.12.2.633),我从相同的转储中得到了这个。我注意到两件事:

数据似乎是HeapAlloc / RegionUsageHeap和VirtualAlloc / RegionUsageIsVAD的总和。

可爱的EFAIL错误无疑在一定程度上导致了数据丢失!

我不确定这对您的托管代码有何帮助,但我认为它实际上回答了最初的问题 ;-)

0:000> !address -summary


Failed to map Heaps (error 80004005)

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unclassified>                         7171          aab21000 (   2.667 Gb)  90.28%   66.68%
Free                                    637          42ecf000 (   1.046 Gb)           26.14%
Stack                                   603           c900000 ( 201.000 Mb)   6.64%    4.91%
Image                                   636           5c21000 (  92.129 Mb)   3.05%    2.25%
TEB                                     201             c9000 ( 804.000 kb)   0.03%    0.02%
ActivationContextData                    14             11000 (  68.000 kb)   0.00%    0.00%
CsrSharedMemory                           1              5000 (  20.000 kb)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                            7921          b49c6000 (   2.822 Gb)  95.53%   70.55%
MEM_IMAGE                               665           5e6e000 (  94.430 Mb)   3.12%    2.31%
MEM_MAPPED                               40           28ed000 (  40.926 Mb)   1.35%    1.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             5734          9b4d1000 (   2.427 Gb)  82.14%   60.67%
MEM_FREE                                637          42ecf000 (   1.046 Gb)           26.14%
MEM_RESERVE                            2892          21c50000 ( 540.313 Mb)  17.86%   13.19%

--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal
PAGE_READWRITE                         4805          942bd000 (   2.315 Gb)  78.37%   57.88%
PAGE_READONLY                           215           3cbb000 (  60.730 Mb)   2.01%    1.48%
PAGE_EXECUTE_READ                        78           2477000 (  36.465 Mb)   1.21%    0.89%
PAGE_WRITECOPY                           74            75b000 (   7.355 Mb)   0.24%    0.18%
PAGE_READWRITE|PAGE_GUARD               402            3d6000 (   3.836 Mb)   0.13%    0.09%
PAGE_EXECUTE_READWRITE                   80            3b0000 (   3.688 Mb)   0.12%    0.09%
PAGE_EXECUTE_WRITECOPY                   80            201000 (   2.004 Mb)   0.07%    0.05%

--- Largest Region by Usage ----------- Base Address -------- Region Size ----------
<unclassified>                                786000           17d9000 (  23.848 Mb)
Free                                        bc480000          38e10000 ( 910.063 Mb)
Stack                                        6f90000             fd000 (1012.000 kb)
Image                                        3c3c000            ebe000 (  14.742 Mb)
TEB                                         fdf8f000              1000 (   4.000 kb)
ActivationContextData                         190000              4000 (  16.000 kb)
CsrSharedMemory                             7efe0000              5000 (  20.000 kb)

2

“使用情况摘要”显示有3696个未分类的区域,总共为17.33 Gb。

“最大的区域”显示未分类区域中最大的区域为242 Mb。其余的未分类区域(3695个)一起占据了剩余的17.33 Gb。

尝试执行!heap -s并对Virt列求和,以查看本机堆的大小,我认为这些也属于未受管理的范畴。 (注意:早期版本会从!address -summary显式显示本机堆)


感谢对Usage Summary和Largest Region的解释。我已经执行了!heap -s命令,并将虚拟列的总和计算为359.12 MB。这是否有什么含义?我已经添加了!heap -s命令的结果。 - Mark
不好意思,由于这是一个64位转储,我没有实际经验。顺便问一下,这个转储是来自哪个版本的.NET?这可能会引起其他人的兴趣,他们可能知道更多信息。 - Kjell Gunnar
我已经在问题描述中添加了该信息。它是.NET 4.0。 - Mark

1
你最好的选择是使用windbg中的EEHeap和GCHandles命令(http://msdn.microsoft.com/en-us/library/bb190764.aspx),并尝试查看是否可以找到可能正在泄漏或错误的内容。
不幸的是,由于诊断这些类型的问题几乎总是非常耗时且除了最简单的情况外需要进行完整分析,因此您可能无法获得您正在寻找的确切帮助。基本上,人们不太可能指向Stack overflow上的直接答案,他们大多数情况下只能指出一些有用的命令。您将不得不进行大量挖掘以获取更多关于正在发生的情况的信息。

只要指向正确的方向就足够了。就像我说的,EEHeap似乎只描述了整个东西的248 MB,所以我不确定答案是否在其中。我会看一下GCHandles。 - Mark

0

最近我花了一些时间来诊断客户的问题,他们的应用程序在终止前使用了70GB的内存(可能是由于达到了IIS应用程序池回收限制,但仍未确认)。他们给我发送了一个35GB的内存转储文件。根据我的最近经验,以下是我对您提供的内容可以做出的一些观察:

在!heap -s输出中,1.247GB中的284MB显示在Commit列中。如果您在DebugDiag中打开此转储文件,它会告诉您堆0x60000有1GB的已提交内存。您将添加报告的11个段的提交大小,并发现它们只加起来约102MB而不是1GB。真是让人恼火。

“缺失”的内存并不缺失。实际上,在!heap -s输出中,它被作为“虚拟块:”行提示。不幸的是,!heap -s很糟糕,无法正确显示结束地址,因此将大小报告为0。请检查以下命令的输出:

!address 17e0000
!address 45bd0000
!address 6fff0000

它将报告正确的结束地址,因此得出准确的“区域大小”。更好的是,它给出了简洁的区域大小版本。如果您将这三个区域的大小加上102MB,您应该就非常接近1GB了。

那么它们里面有什么呢?嗯,您可以使用dq查看。通过探究,您可能会发现为什么分配了它们的提示。也许您的托管代码调用了一些具有本地端的第三方代码。

您可以使用!heap 6fff0000 -x -v找到堆的引用。如果有引用,您可以再次使用!address查看它们所在的内存区域。在我的客户问题中,我发现引用位于一个带有“Usage: Stack”的区域上。一个“More info:”提示引用了栈的线程,该线程在顶部有一些大的basic_string附加/复制调用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接