如何在WPF应用程序中查找内存泄漏

5
我正在尝试识别WPF应用程序中的内存泄漏。该应用程序存在高内存消耗并偶尔抛出OutOfMemoryExceptions异常。但是,该应用程序并不总是显示这些行为。该应用程序是作为x86目标构建的,针对.NET 4.0,并在x64操作系统上运行。
当内存负载较高时,我可以使用procdump.exe在计算机上创建内存转储。转储文件的大小约为1.7 GB。然后,我尝试使用WinDbg进行分析。使用!address -summary命令会输出以下内容,并显示大约1.7 GB的专用字节(MEM_COMMIT):
    --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy    %ofTotal
<unclassified>                         2430          623a1000 (   1.535 Gb)  82.09%   76.74%
Image                                  1583          13dc5000 ( 317.770 Mb)  16.60%   15.52%
Free                                    516           8583000 ( 133.512 Mb)            6.52%
Stack                                   111           18a5000 (  24.645 Mb)   1.29%    1.20%
TEB                                      37             25000 ( 148.000 kb)   0.01%    0.01%
NlsTables                                 1             23000 ( 140.000 kb)   0.01%    0.01%
ActivationContextData                    11             14000 (  80.000 kb)   0.00%    0.00%
CsrSharedMemory                           1              5000 (  20.000 kb)   0.00%    0.00%
PEB                                       1              1000 (   4.000 kb)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                            2144          58bbb000 (   1.386 Gb)  74.16%   69.32%
MEM_IMAGE                              1929          163bf000 ( 355.746 Mb)  18.58%   17.37%
MEM_MAPPED                              102           8af3000 ( 138.949 Mb)   7.26%    6.78%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             3206          6da16000 (   1.713 Gb)  91.62%   85.65%
MEM_RESERVE                             969           a057000 ( 160.340 Mb)   8.38%    7.83%
MEM_FREE                                516           8583000 ( 133.512 Mb)            6.52%

--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal
PAGE_READWRITE                         1605          50166000 (   1.251 Gb)  66.93%   62.57%
PAGE_EXECUTE_READ                       271          11e83000 ( 286.512 Mb)  14.97%   13.99%
PAGE_READONLY                           623           7335000 ( 115.207 Mb)   6.02%    5.63%
PAGE_READWRITE|PAGE_WRITECOMBINE          8           2710000 (  39.063 Mb)   2.04%    1.91%
PAGE_WRITECOPY                          328           15bf000 (  21.746 Mb)   1.14%    1.06%
PAGE_EXECUTE_READWRITE                  210            7ef000 (   7.934 Mb)   0.41%    0.39%
PAGE_EXECUTE_WRITECOPY                   75            181000 (   1.504 Mb)   0.08%    0.07%
PAGE_READWRITE|PAGE_GUARD                82             b5000 ( 724.000 kb)   0.04%    0.03%
<unknown>                                 4              4000 (  16.000 kb)   0.00%    0.00%

--- Largest Region by Usage ----------- Base Address -------- Region Size ----------
<unclassified>                              44150000           22c0000 (  34.750 Mb)
Image                                         802000           243f000 (  36.246 Mb)
Free                                        7a740000            d80000 (  13.500 Mb)
Stack                                        6e10000             fd000 (1012.000 kb)
TEB                                         7eed9000              1000 (   4.000 kb)
NlsTables                                   7efb0000             23000 ( 140.000 kb)
ActivationContextData                          50000              4000 (  16.000 kb)
CsrSharedMemory                             7efe0000              5000 (  20.000 kb)
PEB                                         7efde000              1000 (   4.000 kb)

使用!eeheap -gc命令可以显示如下结果:
Number of GC Heaps: 1
generation 0 starts at 0x78747eb4
generation 1 starts at 0x78741000
generation 2 starts at 0x04781000
ephemeral segment allocation context: none
 segment     begin allocated  size
04780000  04781000  0577fca4  0xffeca4(16772260)
...
78740000  78741000  78841ff4  0x100ff4(1052660)
Large object heap starts at 0x05781000
 segment     begin allocated  size
05780000  05781000  06755a58  0xfd4a58(16599640)
...
52f80000  52f81000  53e07308  0xe86308(15229704)
Total Size:              Size: 0x279b9d7c (664509820) bytes.
------------------------------
GC Heap Size:            Size: 0x279b9d7c (664509820) bytes.

因此,托管堆只有约633 MB的大小,因此托管堆和专用字节之间存在大约1 GB的差距 - 在我看来,这表明存在未经管理的泄漏。由于我们在应用程序中使用P/Invoke和COM互操作,这可能是一个好的起点。我尝试了 !heap -s,它显示以下输出:
LFH Key                   : 0x2c96c29b
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: 15f20000 - 15f20000 (size 00000000)
...
Virtual block: 69cd0000 - 69cd0000 (size 00000000)
02de0000 00000002   81152  66472  81152    738   397     9   56      1   LFH
00720000 00001002    1088    136   1088     16     9     2    0      0   LFH
00470000 00041002     256      4    256      2     1     1    0      0      
Virtual block: 1b150000 - 1b150000 (size 00000000)
...
Virtual block: 6b0d0000 - 6b0d0000 (size 00000000)
00180000 00001002   15424  11932  15424     61   163     5   44      0   LFH
00030000 00001002    1088    136   1088     16     9     2    0      0   LFH
00680000 00041002     256      4    256      0     1     1    0      0      
045e0000 00001002   15424  14092  15424     23    29     5    0      0   LFH
00410000 00041002    1280    308   1280      1     2     2    0      0   LFH
04590000 00041002     256    120    256      1    19     1    0      0   LFH
06820000 00001002     256    108    256      6     4     1    0      0   LFH
09d80000 00001002     256    120    256      1    12     1    0      0   LFH
09c80000 00001002     256     96    256      5     6     1    0      0   LFH
09d10000 00011002     256     12    256      9     5     1    0      0      
09f50000 00001002     256      4    256      1     2     1    0      0      
0a990000 00001002    3136   2444   3136    819    78     3    0      0   LFH
    External fragmentation  33 % (78 free blocks)
0a8d0000 00001002   15424  11792  15424   3620   140     5    0      0   LFH
    External fragmentation  30 % (140 free blocks)
0ad20000 00001002     256    156    256     81     2     1    0      0      
0abe0000 00001002     256    104    256     11     6     1    0      0   LFH
125a0000 00001002    3136   2936   3136    520   102     3    0      0   LFH
    External fragmentation  17 % (102 free blocks)
12330000 00001002      64      8     64      4     6     1    0      0      
0cda0000 00001002      64     12     64      3     1     1    0      0      
280e0000 00001003     256     76    256     49    13     1    0    bad      
28e10000 00001003     256      4    256      2     1     1    0    bad      
29900000 00001003     256      4    256      2     1     1    0    bad      
40190000 00001003     256      4    256      2     1     1    0    bad      
10dd0000 00001003     256      4    256      2     1     1    0    bad      
5b0d0000 00001003    1280    648   1280    627    18     2    0    bad      
5bc70000 00001003     256      4    256      2     1     1    0    bad      
30e90000 00001003     256      4    256      2     1     1    0    bad      
5be60000 00001003     256      4    256      2     1     1    0    bad      
30e50000 00001003     256      4    256      2     1     1    0    bad      
5b330000 00001002      64      8     64      5     1     1    0      0      
5b450000 00001002      64     12     64      3     2     1    0      0      
5b5a0000 00001002      64     12     64      1     4     1    0      0      
5db00000 00001002    3136   1572   3136      6    13     3    0      0   LFH
610a0000 00001002     256      4    256      2     1     1    0      0      
612d0000 00001003      64      8     64      2     2     1    0    bad      
-----------------------------------------------------------------------------

如果我将所有堆(已提交)的大小相加,总和远低于1 GB。为什么堆大小的总和与托管堆大小和私有字节之间的差距不相关?从这里该怎么办?我可以采取哪些步骤来确定泄漏源?谢谢,Markus
编辑
!finalizequeue 显示以下结果:
SyncBlocks to be cleaned up: 3
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 99 finalizable objects (729e88bc->729e8a48)
generation 1 has 8 finalizable objects (729e889c->729e88bc)
generation 2 has 844319 finalizable objects (726b0020->729e889c)
Ready for finalization 1560068 objects (729e8a48->72fdc258)

切换到终结器线程并执行kb返回以下结果:

ChildEBP RetAddr  Args to Child              
06a0f090 76cc149d 00000748 00000000 00000000 ntdll!NtWaitForSingleObject+0x15
06a0f0fc 76491194 00000748 ffffffff 00000000 KERNELBASE!WaitForSingleObjectEx+0x98
06a0f114 76491148 00000748 ffffffff 00000000 kernel32!WaitForSingleObjectExImplementation+0x75
06a0f128 75c87690 00000748 ffffffff 09692d60 kernel32!WaitForSingleObject+0x12
06a0f14c 75daa4d1 02e69078 02e743e0 06a0f258 ole32!GetToSTA+0xad [d:\w7rtm\com\ole32\com\dcomrem\chancont.cxx @ 133]
06a0f17c 75dacef0 06a0f244 06a0f36c 09692d60 ole32!CRpcChannelBuffer::SwitchAptAndDispatchCall+0x140 [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 4419]
06a0f25c 75ca9d01 09692d60 06a0f36c 06a0f354 ole32!CRpcChannelBuffer::SendReceive2+0xef [d:\w7rtm\com\ole32\com\dcomrem\channelb.cxx @ 4076]
06a0f2d8 75ca9b24 09692d60 06a0f36c 06a0f354 ole32!CAptRpcChnl::SendReceive+0xaf [d:\w7rtm\com\ole32\com\dcomrem\callctrl.cxx @ 603]
06a0f32c 75dace06 09692d60 06a0f36c 06a0f354 ole32!CCtxComChnl::SendReceive+0x1c5 [d:\w7rtm\com\ole32\com\dcomrem\ctxchnl.cxx @ 734]
06a0f348 7635420b 0973866c 06a0f398 763d0149 ole32!NdrExtpProxySendReceive+0x49 [d:\w7rtm\com\rpc\ndrole\proxy.cxx @ 1932]
06a0f354 763d0149 6bb28307 06a0f7a0 0700022b rpcrt4!NdrpProxySendReceive+0xe
06a0f768 75dac8e2 75cbfa10 75cc4670 06a0f7a0 rpcrt4!NdrClientCall2+0x1a6
06a0f788 75ca98ad 00000010 00000005 06a0f7c0 ole32!ObjectStublessClient+0xa2 [d:\w7rtm\com\rpc\ndrole\i386\stblsclt.cxx @ 474]
06a0f798 75cab641 0973866c 00000002 02e847d8 ole32!ObjectStubless+0xf [d:\w7rtm\com\rpc\ndrole\i386\stubless.asm @ 154]
06a0f7c0 75cab5ed 0973866c 00000000 00000000 ole32!RemoteReleaseRifRefHelper+0xa5 [d:\w7rtm\com\ole32\com\dcomrem\marshal.cxx @ 6770]
06a0f7fc 75cab172 09704bac 02e69078 00000002 ole32!RemoteReleaseRifRef+0xb0 [d:\w7rtm\com\ole32\com\dcomrem\marshal.cxx @ 6694]
06a0f880 75caa66e 09704bac 09704ba8 00000000 ole32!CStdMarshal::DisconnectCliIPIDs+0x2ec [d:\w7rtm\com\ole32\com\dcomrem\marshal.cxx @ 3964]
06a0f8b0 75caa817 00000002 09704c50 09704ba8 ole32!CStdMarshal::Disconnect+0x1ba [d:\w7rtm\com\ole32\com\dcomrem\marshal.cxx @ 3273]
06a0f8cc 75caa781 09704ba8 06a0f8ec 75caaaf3 ole32!CStdIdentity::~CStdIdentity+0x8c [d:\w7rtm\com\ole32\com\dcomrem\stdid.cxx @ 312]
06a0f8d8 75caaaf3 00000001 0f144b38 0f1fbe44 ole32!CStdIdentity::`scalar deleting destructor'+0xd
06a0f8ec 75dad380 80000000 06a0f904 6f8a4db7 ole32!CStdIdentity::CInternalUnk::Release+0x6e [d:\w7rtm\com\ole32\com\dcomrem\stdid.cxx @ 767]
06a0f8f8 6f8a4db7 0f1fbe44 06a0f948 6f8a4e26 ole32!IUnknown_Release_Proxy+0x11 [d:\w7rtm\com\rpc\ndrole\proxy.cxx @ 1773]
06a0f904 6f8a4e26 0f1fbe44 6a4e83f9 0f144b38 clr!ReleaseTransitionHelper+0xe
06a0f948 6f8a4e8a 0f1fbe44 0f144b18 6a4e8331 clr!SafeReleaseHelper+0x87
06a0f980 6f9f8dc8 0f1fbe44 0f144b18 00000001 clr!SafeRelease+0x2f
06a0f998 6f9f8e96 6a4e8379 00000001 0f144b18 clr!RCW::ReleaseAllInterfaces+0x4b
06a0f9c8 6f9f8ec2 0f144b18 6a4e834d 00000001 clr!RCW::ReleaseAllInterfacesCallBack+0xc4
06a0f9fc 6f9f9105 06a0fa54 06a0fa38 6f9f85d3 clr!RCW::Cleanup+0x22
06a0fa08 6f9f85d3 0f144b18 6a4e8089 06a0fa60 clr!RCWCleanupList::ReleaseRCWListRaw+0x18
06a0fa38 6f9f8a8a 00000001 6a4e8039 6fe995c8 clr!RCWCleanupList::ReleaseRCWListInCorrectCtx+0x10a
06a0fa88 6f9e14f6 6a4e8065 00000000 02e19cb0 clr!RCWCleanupList::CleanupAllWrappers+0x83
06a0fad4 6f9df457 00000001 06a0fc20 6f8f76af clr!SyncBlockCache::CleanupSyncBlocks+0xde
06a0fae0 6f8f76af 02e19cb0 06a0fc20 02e012f8 clr!Thread::DoExtraWorkForFinalizer+0x3b
06a0faf4 6fa1f815 00000001 06a0fbd8 02e19cb0 clr!WKS::GCHeap::FinalizerThreadWorker+0x9d
06a0fb08 6fa1f897 06a0fc20 6a4e8109 06a0fc20 clr!Thread::DoExtraWorkForFinalizer+0x114
06a0fbb8 6fa1f952 06a0fc20 6a4e86a9 00000000 clr!Thread::ShouldChangeAbortToUnload+0x101
06a0fc18 6fa15c57 00000000 02e07688 00000000 clr!Thread::ShouldChangeAbortToUnload+0x399
06a0fc3c 6fa15c6a 6f8f7624 00000008 06a0fc84 clr!ManagedThreadBase_NoADTransition+0x35
06a0fc4c 6f9c0186 6f8f7624 6a4e8635 00000000 clr!ManagedThreadBase::FinalizerBase+0xf
06a0fc84 6fa1f618 00000000 00000000 00000000 clr!WKS::GCHeap::FinalizerThreadStart+0x10c
06a0fd1c 764933aa 02e012f8 06a0fd68 775f9f72 clr!Thread::intermediateThreadProc+0x4b
06a0fd28 775f9f72 02e012f8 71c8758e 00000000 kernel32!BaseThreadInitThunk+0xe
06a0fd68 775f9f45 6fa1f5d0 02e012f8 00000000 ntdll!__RtlUserThreadStart+0x70
06a0fd80 00000000 6fa1f5d0 02e012f8 00000000 ntdll!_RtlUserThreadStart+0x1b

我理解需要释放 RCW,因此需要切换或停止主 STA 线程。然而这会带来新的问题:如何确定正在被完成的对象(RCW 后面的类型是什么)以及为什么 GetToSTA 需要等待(它在等待什么)?


Visual Studio 2013现在有一个新的堆视图,显然它允许您比较两个不同的堆转储并查看正在分配哪些额外对象以及它们的位置。我自己还没有尝试过,也不知道它是否在Express版本中,但是这篇关于它的文章(http://dotnet.dzone.com/articles/visual-studio-2013-heap-view)提供了相关信息。也许值得研究一下? - Mark Feldman
2个回答

2

1
我可以建议两个操作:
  1. 检查应用程序创建的图像。它们的数据位于非托管内存中,因此尽管图像数量较少且未被GC收集(GC1代未达到阈值),但它们占用的内存可能相当大。
  2. 检查被阻塞的终结线程问题 - 在WinDbg中执行!FinalizeQueue并检查可终结对象的数量。

我读了你的文章,发现它们非常有用。事实上,似乎finalization线程被阻塞了。_!FinalizeQueue_显示有超过一百万个对象准备进行终结。终结器线程堆栈显示了一些对RCWs的调用,并停在ole32!GetToSTA+0xad处。我该如何找出正在被终结的具体对象以及为什么GetToSTA需要等待? - Markus Stehle
我不知道如何做。在我的代码中,问题只是通过在循环中的STA线程中添加GC.WaitForPendingFinalizers()来解决(或类似的方法),所以我没有深入到这个层面的细节。 - Alex Netkachov

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