65.9K
CodeProject 正在变化。 阅读更多。
Home

Finalizer 的用法详解

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (12投票s)

2018年4月29日

CPOL

6分钟阅读

viewsIcon

15428

downloadIcon

230

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.exeC:\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

关注点

在本文中,我们学习了:

  1. 托管堆概念和终结器的工作原理
  2. 如何使用Windows内置任务管理器生成进程转储文件
  3. 如何使用各种命令分析转储文件
  4. 切勿在终结器中执行可疑或复杂的代码,这可能会引入意外的错误。

历史

  • 2018年4月29日:初版
终结器案例详解 - CodeProject - 代码之家
© . All rights reserved.