Finalizer 的用法详解
Finalizer 的用法详解
引言
本文介绍如何使用Windbg和.NET调试扩展SOS对.NET应用程序进行事后内存问题调试。这发生在真实的生产环境中,用户遇到了应用程序异常的内存使用问题,而我无法进入他们的环境进行实时调试,因此这篇文章主要分享事后调试的经验。
虽然不能分享出现此问题的代码,但我从《Advanced .NET Debugging》这本书中找到一个与我的场景大致相同的章节。因此,以下内容基于本书中的示例代码。但操作过程是相同的。
背景
我们的一位客户反复抱怨我们的应用程序以异常的方式占用了大量内存。我们尝试了几种方法来调查此案例,但都无济于事:日志分析、系统诊断、系统事件日志分析等。
我们采取的最后手段是让他们在症状出现时使用任务管理器对应用程序进行内存转储,并将转储文件发送给我们。
幸运的是,最终我们确定了根本原因并为用户提供了补丁。分析转储文件的过程以及在此过程中获得的启发促成了这篇博文。
一些基本概念
终结器
有时,封装其他资源的对象的销毁需要清理这些资源。一个很好的例子是封装底层本地资源(如文件句柄)的对象。如果没有显式的清理代码,托管对象背后的内存会被GC清理,但对象封装的底层句柄不会(因为GC不了解本地句柄)。
其结果自然是资源泄漏。为了提供适当的清理机制,CLR引入了终结器。终结器可以与本地C++世界中的析构函数进行比较。每当一个对象被释放(或垃圾回收)时,析构函数(或终结器)就会运行。
在C#中,终结器声明与C++析构函数非常相似,使用~<类名>()的表示法。以下列表显示了一个示例。
public class MyClass
{
~MyClass()
{
// Cleanup code
}
}
当类被编译成IL时,finalize
方法会被翻译成一个名为Finalize
的函数。
为了跟踪哪些对象具有终结器,垃圾回收器维护一个称为终结器队列的队列
队列,其中包含托管堆上所有存活的具有终结器的对象。
当一个具有终结器的对象变成无根对象,并且发生垃圾回收时,GC会将该对象(定义了终结器并被视为垃圾)放入另一个称为f-reachable队列的队列中。
f-reachable队列中每个对象的终结器代码不会作为垃圾回收的一部分执行
阶段。
相反,每个.NET进程都有一个特殊的线程,称为终结器线程,GC会不时唤醒它,并从队列中逐个执行finalizer
方法。
在finalizer
方法执行完毕后,对象将从f-reachable队列中移除,被视为无根对象,并有资格被GC回收。
调试步骤
首先,创建有问题的应用程序的转储文件。
我们将使用Windows内置的任务管理器来创建转储。需要注意的是,如果应用程序是32位,那么taskmgr.exe(C:\Windows\SysWOW64\Taskmgr.exe)必须是32位的,并且Windbg.exe也必须是32位的版本。64位应用程序也一样。
附注:目前,只能将Windbg作为Windows SDK的一部分安装,而Windows SDK非常大。但是,在http://codemachine.com/downloads.html上有Windbg的独立安装程序。
因此,将使用“创建转储文件”来创建转储文件,如下所示:
然后,我们将使用Windbg打开新创建的转储文件,如下所示:
我们需要做的第一件事是加载.NET调试器扩展,执行以下命令:
.loadby sos.dll clr
为了避免指定SOS调试器扩展的完整路径,我们使用.loadby
。其语法如下:
.loadby <extension DLL> <module name>
其中extension DLL代表我们要加载的调试器扩展的名称——在本例中是SOS.DLL。module name代表当前加载的模块——在本例中是clr
。.loadby
命令然后尝试从模块所在的相同路径加载扩展DLL。
在加载SOS后,让我们首先检查与AppDomains和JIT堆相关的各种私有堆。
0:000> !eeheap -loader
Loader Heap:
--------------------------------------
System Domain: 6ee2a288
LowFrequencyHeap: 035b0000(3000:1000) Size: 0x1000 (4096) bytes.
HighFrequencyHeap: 035b4000(9000:1000) Size: 0x1000 (4096) bytes.
StubHeap: 035bd000(3000:1000) Size: 0x1000 (4096) bytes.
Virtual Call Stub Heap:
IndcellHeap: 03920000(2000:1000) Size: 0x1000 (4096) bytes.
LookupHeap: 03925000(2000:1000) Size: 0x1000 (4096) bytes.
ResolveHeap: 0392b000(5000:1000) Size: 0x1000 (4096) bytes.
DispatchHeap: 03927000(4000:1000) Size: 0x1000 (4096) bytes.
CacheEntryHeap: Size: 0x0 (0) bytes.
Total size: Size: 0x7000 (28672) bytes.
--------------------------------------
Shared Domain: 6ee29f38
LowFrequencyHeap: 035b0000(3000:1000) Size: 0x1000 (4096) bytes.
HighFrequencyHeap: 035b4000(9000:1000) Size: 0x1000 (4096) bytes.
StubHeap: 035bd000(3000:1000) Size: 0x1000 (4096) bytes.
Virtual Call Stub Heap:
IndcellHeap: 03920000(2000:1000) Size: 0x1000 (4096) bytes.
LookupHeap: 03925000(2000:1000) Size: 0x1000 (4096) bytes.
ResolveHeap: 0392b000(5000:1000) Size: 0x1000 (4096) bytes.
DispatchHeap: 03927000(4000:1000) Size: 0x1000 (4096) bytes.
CacheEntryHeap: Size: 0x0 (0) bytes.
Total size: Size: 0x7000 (28672) bytes.
--------------------------------------
Domain 1: 035f2320
LowFrequencyHeap: 035c0000(3000:3000) Size: 0x3000 (12288) bytes.
HighFrequencyHeap: 035c3000(a000:5000) Size: 0x5000 (20480) bytes.
StubHeap: Size: 0x0 (0) bytes.
Virtual Call Stub Heap:
IndcellHeap: Size: 0x0 (0) bytes.
LookupHeap: Size: 0x0 (0) bytes.
ResolveHeap: 0391a000(6000:1000) Size: 0x1000 (4096) bytes.
DispatchHeap: 03917000(3000:1000) Size: 0x1000 (4096) bytes.
CacheEntryHeap: 03912000(4000:1000) Size: 0x1000 (4096) bytes.
Total size: Size: 0xb000 (45056) bytes.
--------------------------------------
Jit code heap:
LoaderCodeHeap: 00000000(0:0) Size: 0x0 (0) bytes.
Total size: Size: 0x0 (0) bytes.
--------------------------------------
Module Thunk heaps:
Module 6d311000: Size: 0x0 (0) bytes.
Module 035c4014: Size: 0x0 (0) bytes.
Module 51621000: Size: 0x0 (0) bytes.
Module 6c5e1000: Size: 0x0 (0) bytes.
Total size: Size: 0x0 (0) bytes.
--------------------------------------
Module Lookup Table heaps:
Module 6d311000: Size: 0x0 (0) bytes.
Module 035c4014: Size: 0x0 (0) bytes.
Module 51621000: Size: 0x0 (0) bytes.
Module 6c5e1000: Size: 0x0 (0) bytes.
Total size: Size: 0x0 (0) bytes.
--------------------------------------
Total LoaderHeap size: Size: 0x19000 (102400) bytes.
=======================================
Loader堆大小为102400字节,这似乎正常。接下来,我们看一下托管堆。
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x14921a90
generation 1 starts at 0x148ced8c
generation 2 starts at 0x053c1000
ephemeral segment allocation context: none
segment begin allocated size
053c0000 053c1000 063bf260 0xffe260(16769632)
08cf0000 08cf1000 09cee9a4 0xffd9a4(16767396)
0af10000 0af11000 0bf0e554 0xffd554(16766292)
0d180000 0d181000 0e17ea30 0xffda30(16767536)
0f150000 0f151000 1014e680 0xffd680(16766592)
10680000 10681000 1167f32c 0xffe32c(16769836)
12650000 12651000 13306a20 0xcb5a20(13326880)
14620000 14621000 14922ff4 0x301ff4(3153908)
Large object heap starts at 0x063c1000
segment begin allocated size
063c0000 063c1000 063c5508 0x4508(17672)
Total Size: Size: 0x6fae450 (117105744) bytes.
------------------------------
GC Heap Size: Size: 0x6fae450 (117105744) bytes.
托管堆的总大小约为117MB,这很可疑,考虑到应用程序即将退出并且之前强制进行了垃圾回收,我们需要再仔细看看。
0:000> !DumpHeap -stat
Statistics:
MT Count TotalSize Class Name
...
035c4f3c 1 12 Finalizer.Worker
035c4d68 1 12 Finalizer.Program
6d82ffe4 10 17848 System.Object[]
51645130 10000 120000 System.Management.IWbemClassObjectFreeThreaded
6d82ce08 10000 160000 System.__ComObject
035c4e1c 10000 160000 Finalizer.Wmi
6d823bfc 10001 240024 System.Collections.ArrayList
51644f4c 10000 280000 System.Management.ManagementScope
516433c4 20001 320016 System.Management.WbemDefPath
516413f0 20001 400020 System.Management.ManagementPath
51642bfc 10000 480000 System.Management.ManagementNamedValueCollection
6d832ae8 10015 480816 System.Collections.Hashtable+bucket[]
6d8329f8 10015 520780 System.Collections.Hashtable
5164401c 10000 560000 System.Management.ConnectionOptions
516445dc 10000 640000 System.Management.ManagementClass
51640e94 60000 1920000 System.Management.IdentifierChangedEventHandler
035e1e98 39228 10578600 Free
6d83419c 10001 100120269 System.Byte[]
Total 249927 objects
我们可以看到,我们仍然有10000个Wmi实例和10001个Byte[]
实例。
0:000> !DumpHeap -type Finalizer.Wmi
Address MT Size
053c5c10 035c4e1c 16
053cc57c 035c4e1c 16
148e5344 035c4e1c 16
148e883c 035c4e1c 16
148ebd34 035c4e1c 16
...
1491a2c4 035c4e1c 16
1491d7bc 035c4e1c 16
14920cb4 035c4e1c 16
Statistics:
MT Count TotalSize Class Name
035c4e1c 10000 160000 Finalizer.Wmi
Total 10000 objects
然后,让我们找到一个特定对象的根。
0:000> !gcroot 14920cb4
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 299c
Scan Thread 2 OSTHread 27d4
Finalizer queue:Root:14920cb4(Finalizer.Wmi)
所以该对象被f-reachable队列持有,详细信息如下:
0:000> !finalizequeue
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 3 finalizable objects (124e4050->124e405c)
generation 1 has 3 finalizable objects (124e4044->124e4050)
generation 2 has 13 finalizable objects (124e4010->124e4044)
Ready for finalization 29998 objects (124e405c->12501514)
Statistics for all finalizable objects (including all objects ready for finalization):
MT Count TotalSize Class Name
6d8342f4 1 20 Microsoft.Win32.SafeHandles.SafePEFileHandle
6d832970 1 20 Microsoft.Win32.SafeHandles.SafeFileHandle
6d8247a0 1 20 Microsoft.Win32.SafeHandles.SafeWaitHandle
6d7ef8f8 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
6d7ef8a8 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
6d8307e4 1 52 System.Threading.Thread
6d814f18 1 60 System.Runtime.Remoting.Contexts.Context
6d8323dc 2 88 System.Threading.ReaderWriterLock
6d82df74 8 160 Microsoft.Win32.SafeHandles.SafeRegistryHandle
51645130 10000 120000 System.Management.IWbemClassObjectFreeThreaded
035c4e1c 10000 160000 Finalizer.Wmi
516445dc 10000 640000 System.Management.ManagementClass
Total 30017 objects
为什么这29998个对象没有被终结?让我们检查终结器线程。
0:000> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 299c 035f83a0 2a020 Preemptive 149224A8:00000000 035f2320 1 MTA
2 2 27d4 03607bd8 2b220 Preemptive 00000000:00000000 035f2320 0 MTA (Finalizer)
0:000> ~2s
eax=00000000 ebx=074bf5cc ecx=00000000 edx=00000000 esi=00000000 edi=074bf5b8
eip=7760048c esp=074bf574 ebp=074bf590 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!NtWaitForAlertByThreadId+0xc:
7760048c c20800 ret 8
0:002> k
# ChildEBP RetAddr
00 074bf570 775bd1d9 ntdll!NtWaitForAlertByThreadId+0xc
01 074bf590 775bd11d ntdll!RtlpWaitOnAddressWithTimeout+0x33
02 074bf5d4 775bd016 ntdll!RtlpWaitOnAddress+0xa5
03 074bf610 775da38a ntdll!RtlpWaitOnCriticalSection+0xab
04 074bf630 775da259 ntdll!RtlpEnterCriticalSectionContended+0x12a
*** WARNING: Unable to verify checksum for native.dll
05 074bf638 5ce21564 ntdll!RtlEnterCriticalSection+0x49
06 074bf710 03990757 native!UnInit+0x34
[c:\from mac\download\adndsrc\chapter5\native\05native\05native.cpp @ 45]
WARNING: Frame IP not in any known module. Following frames may be wrong.
07 074bf750 039908ec 0x3990757
08 074bf770 6e94b9c2 0x39908ec
09 074bf7c4 6e94bab5 clr!FastCallFinalize+0x6d
0a 074bf7e8 6e94b83c clr!MethodTable::CallFinalizer+0x139
0b 074bf7f8 6e94b8b3 clr!CallFinalizer+0xa6
0c 074bf848 6e94b915 clr!FinalizerThread::DoOneFinalization+0x84
0d 074bf87c 6e94ad11 clr!FinalizerThread::FinalizeAllObjects+0xa6
0e 074bf8ac 6e86d03a clr!FinalizerThread::FinalizerThreadWorker+0xed
0f 074bf8c0 6e86d0a4 clr!ManagedThreadBase_DispatchInner+0x71
10 074bf964 6e86d171 clr!ManagedThreadBase_DispatchMiddle+0x7e
11 074bf9c0 6e8f1ad8 clr!ManagedThreadBase_DispatchOuter+0x5b
12 074bf9e8 6e8f1b9f clr!ManagedThreadBase::FinalizerBase+0x33
13 074bfa24 6e7cedf1 clr!FinalizerThread::FinalizerThreadStart+0xd4
14 074bfac8 74c28654 clr!Thread::intermediateThreadProc+0x55
15 074bfadc 775f4b17 kernel32!BaseThreadInitThunk+0x24
16 074bfb24 775f4ae7 ntdll!__RtlUserThreadStart+0x2f
17 074bfb34 00000000 ntdll!_RtlUserThreadStart+0x1b
所以此时可以得出结论,finalize
方法被阻塞了!
0:002> !clrstack
OS Thread Id: 0x27d4 (2)
Child SP IP Call Site
074bf71c 7760048c [InlinedCallFrame: 074bf71c]
074bf718 03990757 DomainBoundILStubClass.IL_STUB_PInvoke()
074bf71c 039908ec [InlinedCallFrame: 074bf71c] Finalizer.Worker.UnInit()
074bf758 039908ec Finalizer.Worker.Finalize()
[C:\Users\mrleo\Documents\Visual Studio 2015\Projects\ConsoleApplication1\Program.cs @ 50]
074bf980 6e94b9c2 [DebuggerU2MCatchHandlerFrame: 074bf980]
本质上,finalizer
线程正在执行我们Worker
类型的Finalize
方法,该方法又使用互操作性服务调用一个本地模块(Native
),该模块又试图进入一个关键区域,但似乎卡住了。
仔细查看源代码发现,我们在Finalizer
类中的Initialize
方法中调用的ProcessData
函数进入了关键区域,但随后从未释放该关键区域。UnInit
函数进而尝试进入关键区域,但由于它已经被锁定,调用将无限期等待。由于Worker
类(也具有Finalize
方法)的使用先于Wmi
类的使用,因此Worker
类的实例在我们的许多Wmi实例的任何一个之前就进入了f-reachable队列,这意味着终结器线程将首先拾取该实例并立即卡住。
现在,由于终结器线程只是串行地拾取f-reachable队列上的对象,任何导致终结器线程失败(或在此情况下卡住)的对象都将阻止终结器线程执行f-reachable队列上其余的Finalize
方法,导致这些对象永远无法被清理和垃圾回收。其结果是内存消耗将无界增长,并最终导致OutOfMemoryException
。
使用Finalize
方法可能非常棘手。必须非常小心,确保Finalize
代码中没有意外的错误(如死锁),因为即使是最小的错误也可能不仅导致特定对象无法清理,还会导致f-reachable队列上所有后续对象也无法清理。其结果是内存消耗将无界增长,并最终导致OutOfMemoryException
。
关注点
在本文中,我们学习了:
- 托管堆概念和终结器的工作原理
- 如何使用Windows内置任务管理器生成进程转储文件
- 如何使用各种命令分析转储文件
- 切勿在终结器中执行可疑或复杂的代码,这可能会引入意外的错误。
历史
- 2018年4月29日:初版