一个“内存泄漏”的解剖学

181

从 .NET 视角来看:

  • 内存泄漏是什么?
  • 如何确定您的应用程序是否泄漏?会有哪些影响?
  • 如何预防内存泄漏?
  • 如果您的应用程序有内存泄漏,进程退出或被终止后问题是否解决?或者在进程结束后,应用程序中的内存泄漏是否对系统上的其他进程产生影响?
  • 那么通过 COM 互操作和/或 P/Invoke 访问的非托管代码呢?
15个回答

112
我见过的最好的解释在免费的编程基础电子书的第7章中。
基本上,在.NET中,当引用对象被根引用并且因此无法进行垃圾回收时,会发生内存泄漏。当您保留超出预期范围的引用时会意外发生这种情况。
当您开始收到OutOfMemoryExceptions或内存使用量超出您预期的范围时(PerfMon具有良好的内存计数器),您将知道您存在泄漏。
了解.NET的内存模型是避免它的最佳方法。具体来说,了解垃圾收集器的工作原理和引用的工作方式-再次参考电子书的第7章。此外,请注意常见陷阱,其中可能最常见的是事件。如果对象A在对象B上注册了事件,则对象A将一直保留,直到对象B消失,因为B持有对A的引用。解决方案是在完成后取消注册事件。
当然,一个好的内存配置文件将让您看到对象图并探索对象的嵌套/引用,以查看引用来自何处以及哪个根对象负责(red-gate ants profile,JetBrains dotMemory,memprofiler 都是非常好的选择,或者您可以使用纯文本的 WinDbgSOS,但我强烈建议除非您是真正的专家,否则使用商业/可视化产品)。
我认为非托管代码会受到其典型的内存泄漏的影响,只是共享引用由垃圾回收器管理。我对这最后一点可能是错误的。

11
哦,你喜欢书对吧?我曾经在Stack Overflow上看到过这位作者。 - Johnno Nolan
一些.NET对象也可以成为根对象并变得不可回收。因此,任何实现了IDisposable接口的对象都应该被处理。 - kyoryu
1
@kyoryu:一个对象如何扎根? - Andrei Rînea
3
@Andrei:我认为一个正在运行的线程可能是最好的自我引用对象的例子。将自己的引用放在静态非公共位置(例如订阅静态事件或通过静态字段初始化实现单例)的对象也可以视为已经自我引用,因为没有明显的方法来……嗯……从其固定位置移动它。 - Jeffrey Hantin
@Jeffry 这是一种不寻常的描述方式,我很喜欢! - Exitos
非托管/本地代码通常更容易受到物理泄漏的影响,但这些逻辑/根源性的影响并不那么明显。这是因为如果我们有其中一个根源资源无法被"拔除",通常它们最终会在像C这样的语言中被显式销毁。在这些情况下发生的错误更常见的是悬空指针,通常与访问它相关的段错误。虽然有时这实际上比让这个根源资源简单地消耗内存直到应用程序关闭(崩溃是非常令人讨厌、易于检测的错误)更可取。 - user4842163

35

严格来说,内存泄漏是程序占用了“不再使用”的内存。

“不再使用”有多种含义,可能意味着“没有引用它”,也就是说,完全无法恢复,或者可能意味着已被引用、可恢复、未使用,但程序仍然保留这些引用。只有后者适用于 .Net 的完美管理对象。然而,并非所有的类都是完美的,在某些情况下,底层的非托管实现可能会永久地泄漏资源。

在所有情况下,应用程序消耗的内存比严格需要的更多。副作用,取决于泄漏的数量,可能从没有到由于过度收集而导致的减速,再到一系列内存异常,最终出现致命错误并强制进程终止。

当监视显示在每个垃圾回收周期后为您的进程分配了越来越多的内存时,您就知道应用程序存在内存问题。在这种情况下,您要么将太多内容保留在内存中,要么底层的非托管实现正在发生泄漏

对于大多数泄漏,当进程终止时,资源将被恢复,但在某些特定情况下,某些资源并不总是会被恢复,例如GDI光标句柄。当然,如果您有一个进程间通信机制,则在其他进程中分配的内存将在该进程释放或终止之前不会被释放。


32

我认为关于“什么是内存泄漏”和“影响有哪些”的问题已经有了很好的回答,但我想在其他问题上添加一些东西...

如何判断你的应用程序是否泄漏

一个有趣的方法是打开 perfmon 并为 # bytes in all heaps# Gen 2 collections 添加跟踪,每种情况下只查看您的进程。如果运行某个特定功能会导致总字节数增加,并且该内存在下一个 Gen 2 收集后仍保持分配状态,则可以说该功能存在内存泄漏。

如何预防

其他人已经提出了很好的观点。我只想补充一点,即 .NET 内存泄漏最常被忽视的原因可能是在对象中添加事件处理程序而没有将它们移除。附加到对象的事件处理程序是对该对象的一种引用,因此即使所有其他引用已经消失,它也会阻止收集。始终记得解除事件处理程序(在 C# 中使用 -= 语法)。

进程退出时泄漏是否消失,COM 互操作呢?

当您的进程退出时,操作系统会回收映射到其地址空间的所有内存,包括从 DLL 中提供的任何 COM 对象。相对较少情况下,COM 对象可以从单独的进程中提供。在这种情况下,当您的进程退出时,您可能仍然需要负责使用的任何 COM 服务器进程中分配的内存。


18
我认为内存泄漏是指对象在完成后未释放所有分配的内存。如果您在应用程序中使用Windows API和COM(即有错误或未正确管理的非托管代码),框架和第三方组件,可能会发生这种情况。我还发现,不清理使用某些对象(如笔)后可能会导致此问题。
我个人遭受过内存不足异常,这可能是由于.NET应用程序中的内存泄漏引起的,但不是唯一的原因。(OOM也可能来自固定参见Pinning Artical)。如果没有收到OOM错误或需要确认是否存在内存泄漏,则唯一的方法是对应用程序进行分析。
我还会尝试确保以下内容:
a)使用finally块或using语句处理实现Idisposable的所有内容,包括刷子、钢笔等。(有些人认为还要将所有内容设置为nothing)
b)任何具有close方法的内容都再次使用finally或using语句关闭(尽管我发现使用并不总是关闭,这取决于您是否在using语句之外声明了对象)
c)如果您正在使用非托管代码/ Windows API,则必须正确处理这些内容后。(有些人有清理方法以释放资源)
希望这可以帮助您。

18

如果你需要在.NET中诊断内存泄漏,请查看以下链接:

http://msdn.microsoft.com/en-us/magazine/cc163833.aspx

http://msdn.microsoft.com/en-us/magazine/cc164138.aspx

那些文章描述了如何创建进程内存转储并分析它,以便首先确定您的泄漏是托管还是非托管的,如果是托管的,如何找出它来自哪里。
微软还有一个新工具来帮助生成崩溃转储,以替代ADPlus,称为DebugDiag。

http://www.microsoft.com/downloads/details.aspx?FamilyID=28bd5941-c458-46f1-b24d-f60151d875a3&displaylang=en


15

14

如何垃圾回收器工作的最好解释在Jeff Richter的CLR via C#书中(第20章)。阅读此书可以很好地理解对象如何持久存在。

意外根据对象的最常见原因之一是通过在类外部连接事件。如果您连接外部事件

例如:

SomeExternalClass.Changed += new EventHandler(HandleIt);

如果你在释放对象时忘记取消挂钩,那么一些外部类仍会引用你的类。

如上所述,SciTech内存分析器非常擅长显示你怀疑正在泄漏的对象的根源。

但也有一种非常快速的方法来检查特定类型,只需要使用WnDBG(甚至可以在连接时使用VS.NET即时窗口):

.loadby sos mscorwks
!dumpheap -stat -type <TypeName>

现在做一些你认为会处理该类型对象的操作(例如关闭一个窗口)。在这里方便的是有一个调试按钮,可以运行System.GC.Collect()几次。然后再次运行!dumpheap -stat -type <TypeName>。如果数字没有降下来,或者降下来的不如你预期,那么你就有了进一步调查的基础。(我从Ingo Rammer的研讨会上得到了这个提示。)

13

我想在托管环境中,泄漏是指你保留了一个不必要的引用到一个大块内存。


11

为什么人们认为.NET中的内存泄漏与其他泄漏不同?

内存泄漏是当您附加到资源并不释放时发生的。您可以在托管和非托管编码中都这样做。

关于.NET和其他编程工具,有一些关于垃圾回收和最小化可能导致应用程序泄漏的情况的想法。 但是预防内存泄漏的最佳方法是需要了解底层的内存模型以及在使用的平台上的工作原理。

相信垃圾回收和其他“魔法”将清理您的混乱是造成内存泄漏的捷径,并且难以在以后找到。

在进行非托管编码时,通常会确保清除,您知道您占用的资源将是您的责任来清除,而不是管理员的责任。

另一方面,在.NET中,很多人认为GC会清理所有内容。好吧,它确实为您做了一些事情,但您需要确保是这样的。.NET确实包装了很多东西,因此您并不总是知道自己是否正在处理托管或非托管资源,您需要确定自己正在处理什么。 处理字体、GDI资源、活动目录、数据库等通常是您需要注意的事情。

从托管的角度来看,我会说一旦进程被杀死/移除,内存泄漏就消失了。

我看到很多人都有这种想法,而我真的希望这种情况能够结束。您不能要求用户终止应用程序以清理您的混乱! 看看浏览器,可以是IE、FF等,然后打开Google Reader,让它停留几天,看看会发生什么。

如果您随后在浏览器中打开另一个标签页,浏览到某个站点,然后关闭托管造成浏览器泄漏的其他页面的选项卡,您认为浏览器会释放内存吗?但IE不会。如果我使用Google Reader,在我的计算机上,IE在短时间内(约3-4天)就会轻松吃掉1 GiB的内存。有些新闻网页甚至更糟。


9

在托管环境中,内存泄漏指的是您保留了一个不必要的对大块内存的引用。

完全正确。此外,当适当时未使用可处理对象的.Dispose()方法也会导致内存泄漏。最简单的方法是使用using块,因为它会自动执行.Dispose():

StreamReader sr;
using(sr = new StreamReader("somefile.txt"))
{
    //do some stuff
}

如果您创建了一个使用非托管对象的类,并且没有正确实现IDisposable,那么您可能会为该类的用户造成内存泄漏。


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