内存泄漏检测 .NET






4.75/5 (83投票s)
垃圾回收和 .NET 中的内存泄漏检测。
引言
通常,检测和定位内存泄漏是非常麻烦的。本文将提供一种定位 .NET 应用程序中内存泄漏的方法。首先,我将讨论资源分配和垃圾回收算法,然后将讨论检测 .NET 应用中的泄漏。请注意代码部分中的粗体文本。
背景
资源分配
CLR(公共语言运行时)在托管堆上分配所有资源,并在应用程序不再需要它们时释放它们。C/C++ 应用程序容易出现内存泄漏,因为程序员必须手动分配和释放内存。
运行时维护一个 NextObjPtr
来指向堆上下一个可用空间。当一个新进程初始化时,CLR 会为该进程在堆上分配一块连续的空间,由 NextObjPtr
表示,并增加 NextObjPtr
指针以指向下一个可用空间。空间是连续的,这与 C++ 堆不同,C++ 堆作为链表维护。与 C++ 堆相比,GC 堆效率更高,因为分配新内存时,GC 无需搜索可用内存列表或链表。随着时间的推移,当对象被删除时,堆中会开始出现间隙,因此 GC 必须进行堆压缩,这成本很高。 .NET 中的 GC 使用 Win32 API VirtualAlloc
或 VirtualAllocEX
来保留内存。
.NET 使用几种类型的内存,如堆栈、非托管堆和托管堆。
- 堆栈: 堆栈是按线程管理的,用于存储局部变量、方法参数和临时值。GC 不清理堆栈,因为当方法返回时,堆栈会自动清理。对象引用存储在堆栈上,但实际对象在堆上分配,GC 也知道这一点。当 GC 找不到对象的引用时,它会将其从堆中删除。
- 非托管堆: 非托管代码会在非托管堆或堆栈上分配对象。托管代码也可以通过调用 Win32 API 在非托管堆上分配对象。
- 托管堆: 托管代码在托管堆上分配对象,GC 负责管理托管堆。GC 还维护一个大对象堆,以补偿移动内存中大对象的成本。
垃圾回收算法
垃圾收集器检查堆中应用程序不再使用的对象。如果存在这样的对象,GC 会将这些对象从堆中删除。现在的问题是,GC 如何找出应用程序未使用的这些对象。每个应用程序都维护一组根。根就像指向堆上对象的指针。所有全局和静态对象指针都被视为应用程序根。线程堆栈上的任何局部变量都被视为应用程序根。这个根列表由 JIT 编译器和 CLR 维护,并提供给 GC。
当 GC 开始运行时,它将所有对象视为垃圾,并假设堆上的所有对象都无法访问。然后,它开始遍历应用程序根列表,并开始构建一个可访问对象的图。它将堆上的所有对象标记为可访问,如果对象直接可访问(如应用程序根)或通过任何其他对象间接可访问。对于每个应用程序,GC 都维护一个引用树,该树跟踪应用程序引用的对象。使用这种方法,GC 构建了一个活动对象列表,然后遍历堆以查找不在该活动对象列表中的对象。在找出不在该活动对象列表中的对象后,它会将它们全部标记为垃圾,并开始压缩内存以清除未引用(死)对象造成的空隙。它使用 memcpy
函数将对象从一个内存位置移动到另一个位置,并修改应用程序根以指向新位置。
如果对象存在活动引用,则称其为强根。 .NET 也有弱引用的概念。任何对象都可以创建为弱引用,它告诉 GC 我们想访问此对象,但如果 GC 正在进行垃圾回收,则可以收集它。弱引用通常用于非常大的对象,这些对象易于创建但维护成本很高。
在内存中移动对象会带来显著的性能损失。为了提高性能,GC 会进行多项优化,例如大对象堆和代。大小超过 85,000 字节的对象被分配到大对象堆。在内存中移动大对象成本很高,因此 GC 为大对象维护一个单独的堆,它从不压缩该堆。GC 还维护对象的代。每当需要分配一个新对象而托管堆没有足够的内存时,就会执行 GC 集合。首次,堆中的每个对象都被视为 Gen 0。之后,GC 执行一次收集。幸存的对象会被移至 Gen 1,同样,那些在 Gen 1 收集中幸存的对象会移至 Gen 2。GC 假设新对象寿命短,旧对象寿命长。每当需要新内存时,GC 会尝试从 Gen 0 收集内存,如果从 Gen 0 收集无法获得足够的内存,则会执行 Gen 1 甚至 Gen 2 收集。
GC 序列
每次 GC 收集期间都会发生以下步骤
- 执行引擎挂起 – EE 会挂起,直到所有托管线程都到达代码执行中被认为是“安全”的点。
- 标记 – 没有根的对象被标记为垃圾。
- 计划 – GC 为正在收集的每一代创建预算,然后确定 GC 收集后托管堆将存在的碎片量。
- 扫描 – 删除所有标记为删除的对象。
- 压缩 – 将所有未被固定的幸存对象移动到堆的低位。
- 执行引擎重启 – 重启托管线程的执行。
GC 中的根
GC 中有不同种类的根
- 强引用 – 如果对象存在强引用,则认为该对象正在使用,并且不会在下一次 GC 收集期间被收集。
- 弱引用 – 这也是一种引用,但对象可以存活到下一次 GC 收集。弱引用就像对象的缓存。
析构
GC 可以跟踪非托管资源的生命周期,但除非使用析构函数或编写代码覆盖基类的 Finalize
,否则它无法回收资源使用的内存。
析构函数用于允许程序员在对象被垃圾回收之前清理对象使用的本机资源。但是,使用析构函数会将对象收集提升到下一代。每当具有 Finalize
方法的新对象在堆上分配时,该对象的指针就会放在析构队列上。在垃圾回收期间,如果 GC 发现对象不可达,它会搜索析构队列中对该对象的任何引用。如果找到引用,它会从析构队列中删除该对象,并将其附加到另一个称为 Freachable 队列的数据结构中。此时,垃圾收集器已完成对垃圾的识别并压缩了内存。之后,析构线程通过执行每个对象的 Finalize
方法来清空 Freachable 队列。下一次执行 GC 收集时,GC 会将此对象视为垃圾,并回收分配给该对象的内存。
从具有 Finalize
方法的对象中回收内存需要更长的时间,并且会影响性能,因此 Finalize
方法应仅在需要时使用。
检测内存泄漏
内存泄漏可能发生在堆栈、非托管堆或托管堆中。有很多方法可以找出内存泄漏,例如任务管理器中内存的增加。在开始纠正内存问题之前,您需要确定泄漏的内存类型。可以使用 Perfmon 来检查计数器,例如 Process/Private bytes、.NET CLR Memory/# bytes in all heaps 以及 .NET CLR LocksAndThreads/# of the current logical thread。如果 .NET CLR LocksAndThreads/# 意外增加,则表示线程堆栈正在泄漏。如果只有 Process/Private bytes 增加而 .NET CLR Memory 没有增加,则表示非托管内存正在泄漏,否则如果两者都增加,则表示托管内存正在泄漏。
堆栈内存
堆栈内存会在方法返回后被回收。堆栈内存可能以两种方式泄漏。首先,方法调用消耗大量堆栈资源但永不返回,从而永远不会释放相关的堆栈帧。另一种是创建后台线程而不终止它们,从而泄漏线程堆栈。
非托管堆内存
如果总内存使用量在增加但 .NET CLR 内存没有增加,则表示非托管内存正在泄漏。非托管内存可能以多种方式泄漏 - 如果托管代码与非托管代码互操作,并且非托管代码中存在泄漏。.NET 不保证每个对象的析构函数都会被调用。在当前实现中,.NET 有一个析构线程。如果存在一个阻塞此线程的析构函数,那么其他析构函数将永远不会被调用,并且本应被释放的非托管内存会泄漏。当 AppDomain 被销毁时,CLR 会尝试运行所有析构函数,但如果存在阻塞的析构函数,则会阻止 CLR 完成 AppDomain 的销毁。为防止这种情况,CLR 对进程实现了一个超时,之后它会停止析构过程,而本应被删除的非托管内存就会泄漏。
托管堆内存
托管内存也可能因多种原因泄漏,例如大对象堆的碎片化。大对象堆中的内存永远不会被压缩,因此那里存在内存损失。此外,如果存在一些不再需要但仍然存在引用的对象,那么 GC 永远不会回收分配给这些对象的内存。
这种泄漏很常见,可以使用 SOS.dll 来解决。使用 SOS.dll 有两种方法
- WinDbg 工具:此工具可以从 Microsoft 下载。
运行您要调试内存问题的应用程序。启动 WinDbg 工具并将其附加到应用程序进程。
或者
- 在 Visual Studio 2005 中打开应用程序。转到项目的属性。在“调试”选项卡中,确保选中了“启用非托管代码调试”,或者如果您附加到进程,则在“附加到进程”窗口中,单击“选择”,然后选择“托管代码”和“本机代码”。运行应用程序,并在代码中设置一个断点,您希望应用程序暂停执行的地方。命中断点。转到调试 -> 窗口 -> 立即窗口。
- 运行 .load SOS.dll
SOS.dll 是用于调试托管代码最流行的调试扩展。它有很多强大的命令,可以获取托管调用堆栈、托管堆详细信息、堆中的对象等信息。
- 运行 !dumpheap –stat 或 !dumpheap –type PolicyEditor
此命令会扫描 GC 堆并列出其中包含的对象。–Stat 参数用于将输出显示为统计摘要。有关此(任何)命令的更多信息,可以使用 !help dumpheap 找到。
输出将如下所示
03f354ec 20 1120 View.Console.Configuration.Workspace
03713e44 35 1120 System.Windows.Forms.LayoutEventHandler
03710bec 56 1120 System.ComponentModel.Container+Site
024dc014 70 1120 System.Configuration.PropertySourceInfo
0492ed48 94 1128 Infragistics.Shared.SubObjectBase+NotifyId
081d9ac4 2 1136 View.Manger.UI.PolicyEditor
03d79898 1 1140 System.Text.RegularExpressions.RegexCharClass+
LowerCaseMapping[]
08857bdc 22 1144 Infragistics.Win.UltraWinToolbars.ComboBoxTool+
ComboBoxToolExtraSharedProps
0869c604 8 1152 Infragistics.Win.UltraWinEditors.UltraTextEditorUIElement
- 上面的命令将列出内存中存在的所有对象。如果您认为某个对象不应该存在于内存中并且应该已被垃圾回收,则打开“查找”(Ctrl +F)窗口并键入对象名称进行搜索。如果您找不到该对象,则表示它已被垃圾回收,或者尚未实例化。如果您找到该对象,例如上面列表中的
PolicyEditor
对象,则复制 MT(方法表)地址,在这种情况下是 **081d9ac4**。 - 运行 !dumpheap -mt 081d9ac4
这将列出所有具有此方法表地址的对象。这些对象是
PolicyEditor
的实例。输出将是这样的
------------------------------
Heap 0
Address MT Size
total 0 objects
------------------------------
Heap 1
Address MT Size
total 0 objects
------------------------------
Heap 2
Address MT Size
28404fd8 081d9ac4 568
total 1 objects
------------------------------
Heap 3
Address MT Size
2c49f098 081d9ac4 568
total 1 objects
------------------------------
total 2 objects
Statistics:
MT Count TotalSize Class Name
081d9ac4 2 1136 View.Manger.UI.PolicyEditor
Total 2 objects
- 运行 !gcroot 28404fd8
这将列出从 GC 树根到该对象的路径。如果该对象存在某条路径,则认为它不是垃圾,并且 GC 不会收集它,这可能是内存泄漏的原因。
输出将如下所示
复制任何实例的地址,例如 **28404fd8**。
Error during command: warning!
Extension is using a feature which Visual does not implement.
Scan Thread 6460 OSTHread 193c
Scan Thread 1884 OSTHread 75c
Scan Thread 7520 OSTHread 1d60
Scan Thread 7716 OSTHread 1e24
Scan Thread 0 OSTHread 0
Scan Thread 7428 OSTHread 1d04
Scan Thread 0 OSTHread 0
Scan Thread 4728 OSTHread 1278
DOMAIN(01D637C8):HANDLE(Pinned):22b148c:Root:302d9250(System.Object[])->
2908633c(System.EventHandler)->
29085f2c(System.Object[])->
2c53a4cc(System.EventHandler)->
2c53a340(Infragistics.Win.UltraWinStatusBar.UltraStatusBar)->
2c5513b0(System.Collections.Hashtable)->
2c5513e8(System.Collections.Hashtable+bucket[])->
2c5390ac(Infragistics.Win.Printing.UltraPrintPreviewControl)->
2c530668(Infragistics.Win.UltraWinGrid.UltraGridPrintDocument)->
2c530858(System.Drawing.Printing.PrintEventHandler)->
2c52eff0(View.Windows.InfragisticsControls.CommonUltraGrid)->
24487ed0(System.EventHandler)->
2c52df64(View.Manger.UI.PolicyClarificationBrowser)->
2448f478(System.EventHandler)->
2c4e3714(View.Manger.UI.PolicyEditor)
DOMAIN(01D637C8):HANDLE(Pinned):22c12f8:Root:302d52b8(System.Object[])->
282e427c(System.Collections.Generic.Dictionary`2[[System.Object, mscorlib],
[System.Collections.Generic.List`1
[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]],
mscorlib]])->
282e4444(System.Collections.Generic.Dictionary`2+Entry[[System.Object, mscorlib],
[System.Collections.Generic.List`1
[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]],
mscorlib]][])->
282ee76c(System.Collections.Generic.List`1
[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]])->
2048e9c4(System.Object[])->
2c500534(Microsoft.Win32.SystemEvents+SystemEventInvokeInfo)->
2c500514(Microsoft.Win32.UserPreferenceChangedEventHandler)->
2c4fff54(Infragistics.Win.UltraWinStatusBar.UltraStatusBar)->
2446c824(System.Collections.Hashtable)->
2446c85c(System.Collections.Hashtable+bucket[])->
2c4fecc0(Infragistics.Win.Printing.UltraPrintPreviewControl)->
2c4f6a24(Infragistics.Win.UltraWinGrid.UltraGridPrintDocument)->
2c4f6c14(System.Drawing.Printing.PrintEventHandler)->
2c4f53ac(View.Windows.InfragisticsControls.CommonUltraGrid)->
2446e90c(System.EventHandler)->
24450e80(View.Manger.UI.EvidenceDefinitionMiniBrowser)->
2c4e34bc(View.Manger.Views.PolicyEditController)->
2c4e3524(View.Manger.Views.PolicyEdit)->
2449cfe0(System.Windows.Forms.LayoutEventArgs)->
2c4e3714(View.Manger.UI.PolicyEditor)
DOMAIN(01D637C8):HANDLE(WeakLn):22c1d88:Root:24496d2c(
System.Windows.Forms.NativeMethods+WndProc)->
2c4e39fc(System.Windows.Forms.Control+ControlNativeWindow)->
2c4e3714(View.Manger.UI.PolicyEditor)
DOMAIN(01D637C8):HANDLE(WeakSh):22e313c:Root:2c4e39fc(
System.Windows.Forms.Control+ControlNativeWindow)
- 在上面的输出中,我们可以看到
PolicyClarificationBrowser
对象有一个事件处理程序,该处理程序持有对PolicyEditor
的引用。要找出是谁在挂接这个事件,请获取事件处理程序的地址,即 **2448f478**,然后转储这个对象。 - !dumpobj 2448f478
Name: System.EventHandler
MethodTable: 7910d61c
EEClass: 790c3a7c
Size: 32(0x20) bytes
(C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
MT Field Offset Type VT Attr Value Name
790f9c18 40000f9 4 System.Object 0 instance 2c4e3714 _target
79109208 40000fa 8 ...ection.MethodBase 0 instance 00000000 _methodBase
790fe160 40000fb c System.IntPtr 0 instance 140144060 _methodPtr
790fe160 40000fc 10 System.IntPtr 0 instance 0 _methodPtrAux
790f9c18 4000106 14 System.Object 0 instance 00000000 _invocationList
790fe160 4000107 18 System.IntPtr 0 instance 0 _invocationCount
- 如果您获取上面输出中的目标地址 2c4e3714,并用该地址运行 !dumpobj,您会在输出中看到它属于
PolicyEditor
类型。要获取被挂接为处理程序的函数,请将_methodPtr
中的整数值转换为十六进制。?0n140144060
(此命令在立即窗口中不起作用。要运行此命令,您需要将 WinDbg 附加到此进程或任何其他托管进程,或者使用 Google 来完成此操作。)
输出将是:
Evaluate expression: 140144060 = 085a6dbc
And then run !ip2md 085a6dbc
Failed to request MethodData, not in JIT code range
- !dumpobj 2c4e3714
输出将是:
有时这会奏效,并会给您函数的名称,但如果不起作用,那么我们可以转储对象然后找出函数。
Name: View.Manger.UI.PolicyEditor
MethodTable: 0820865c
EEClass: 08219fd8
Size: 568(0x238) bytes
(C:\Documents and Settings\testuser\Application Data\View\SMC-D-44725-B\View.Manger.UI.dll)
Fields:
MT Field Offset Type VT Attr Value Name
790f9c18 4000184 4 System.Object 0 instance 00000000 __identity
024c1798 40008bc 8 ...ponentModel.ISite 0 instance 00000000 site
036f9c3c 40008bd c ....EventHandlerList 0 instance 2448f8ac events
790f9c18 40008bb 108 System.Object 0 static 242e1afc EventDisposed
- !dumpmt -md 0820865c
输出将是:
EEClass: 08219fd8
Module: 036e48d8
Name: View.Manger.UI.PolicyEditor
mdToken: 02000069 (C:\Documents and Settings\testuser\Application Data\
View\SMC-D-44725-B\View.Manger.UI.dll)
BaseSize: 0x238
ComponentSize: 0x0
Number of IFaces in IFaceMap: 15
Slots in VTable: 430
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
022a9fc5 022a9ec8 NONE System.ComponentModel.Component.ToString()
793539c0 7913bd50 PreJIT System.Object.Equals(System.Object)
…
…
085a6d44 08208578 NONE View.Manger.UI.PolicyEditor.btnStatementDelete_Click(
System.Object, System.EventArgs)
085a6de4 08208580 NONE View.Manger.UI.PolicyEditor.btnProperties_Click(
System.Object, System.EventArgs)
085a6dbc 08208588 NONE View.Manger.UI.PolicyEditor.ClarificationBrowser_OpenClick(
System.Object, System.EventArgs)
08208f0d 08208590 NONE View.Manger.UI.PolicyEditor.OpenClarification()
085a6dd0 08208598 NONE View.Manger.UI.PolicyEditor.Clarification_SelectionChanged(
System.Object, View.Manger.UI.SelectEventArgs)
085a6d94 082085a0 NONE View.Manger.UI.PolicyEditor.btnExceptionNew_Click(
System.Object, System.EventArgs)
…
…
在上面的输出中,搜索我们从 _methodptr
计算出的十六进制地址,您将获得仍然持有 PolicyEditor
对象引用的函数的名称,并且是内存泄漏的原因。
历史
- 最初发布于 2007 年 7 月 3 日。