“内存泄漏”的剖析


172

在.NET透视图中:

  • 什么是内存泄漏
  • 您如何确定您的应用程序是否泄漏?有什么影响?
  • 如何防止内存泄漏?
  • 如果您的应用程序存在内存泄漏,那么在进程退出或被杀死时它会消失吗?还是即使进程完成后,应用程序中的内存泄漏也会影响系统上的其他进程吗?
  • 通过COM Interop和/或P / Invoke访问的非托管代码又如何呢?

Answers:


110

我所见过的最好的解释是在免费的《编程基础》电子书的第7章中。

基本上,在.NET中,当引用的对象植根时会发生内存泄漏,因此无法进行垃圾回收。当您保留超出预期范围的引用时,会意外发生。

您将知道开始获取OutOfMemoryExceptions时发生泄漏,或者内存使用量超出了您的预期(PerfMon具有不错的内存计数器)。

了解.NET的内存模型是避免它的最佳方法。具体而言,了解垃圾收集器的工作方式和引用的工作方式-再次,请您参考电子书的第7章。另外,请注意常见的陷阱,可能是最常见的事件。如果将对象A注册到对象B上的事件,则对象A会一直停留到对象B消失,因为B拥有对A的引用。解决方案是在完成后注销事件。

当然,良好的内存配置文件将使您能够查看对象图并探索对象的嵌套/引用,以查看引用来自何处以及由哪个根对象负责(red-gate蚂蚁配置文件,JetBrains dotMemory,memprofiler确实很棒选择,或者您可以使用纯文本的WinDbgSOS,但是除非您是真正的专家,否则我强烈建议您使用商业/视觉产品)。

我相信非托管代码会遭受其典型的内存泄漏,除了共享引用是由垃圾收集器管理之外。最后一点我可能是错的。


11
哦,你喜欢书吗?我看到作者不时出现在stackoverflow上。
约翰诺·诺兰

某些.NET对象也可以自身生根并变得无法收集。因此,应该处理任何IDisposable的东西。
kyoryu 2010年

1
@kyoryu:对象如何生根?
安德烈·雷内(AndreiRînea),2010年

2
@Andrei:我认为正在运行的Thread也许是对象生根的最佳示例。将引用置于自身的静态非公共位置(例如订阅静态事件或通过静态字段初始化实现单例)的对象也可能已经扎根,因为没有明显的方法... um ...从锚泊处“拔掉”它。
杰弗里·汉汀

@Jeffry这是描述正在发生的事情的非常规方式,我喜欢它!
Exitos

35

严格来说,内存泄漏正在消耗程序“不再使用”的内存。

“不再使用”的含义不只一种,可能意味着“不再引用”,即完全不可恢复,或者可能意味着已引用,可恢复,未使用,但程序仍会保留引用。只有后者适用于.Net才能完美管理对象。但是,并非所有类都是完美的,在某些时候,底层的非托管实现可能会永久泄漏该进程的资源。

在所有情况下,应用程序消耗的内存都比严格需要的更多。副作用(取决于泄漏的数量)可能从无到由过度收集导致的减慢,到一系列内存异常,最后是致命错误,然后强制终止进程。

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

对于大多数泄漏,资源是在过程终止时恢复的,但是在某些精确的情况下,某些资源并不总是可以恢复的,因此GDI游标句柄是众所周知的。当然,如果您具有进程间通信机制,则在该进程释放它或终止该进程之前,不会释放在其他进程中分配的内存。


32

我认为“什么是内存泄漏”和“什么是影响”问题已经很好地回答了,但是我想在其他问题上再添加一些内容...

如何了解您的应用程序是否泄漏

一种有趣的方法是打开perfmon并为所有堆#Gen 2集合中的#个字节添加跟踪,在每种情况下仅查看您的进程。如果行使一项特定功能导致总字节数增加,并且该内存在下一次​​第二代收集之后仍保持分配状态,则您可能会说该功能泄漏了内存。

如何预防

还给出了其他好的意见。我只想补充一点,.NET内存泄漏的最常见的原因可能是在不删除对象的情况下向对象添加了事件处理程序。附加到对象的事件处理程序是对该对象的一种引用形式,因此即使在所有其他引用都消失之后,它也会阻止收集。始终记住要分离事件处理程序(使用-=C#中的语法)。

当进程退出时,泄漏会消失吗?COM互操作又如何呢?

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


19

我将内存泄漏定义为一个对象,该对象在完成后不会释放所有分配的内存。我发现如果在框架和第三方组件中使用Windows API和COM(即其中包含错误或未正确管理的非托管代码),则可能在应用程序中发生这种情况。我还发现在使用某些物体(例如笔)后不整理可能会引起问题。

我个人遭受了内存不足异常的影响,这可能是由于内存异常引起的,但并非仅限于点网应用程序中的内存泄漏。(OOM也可以来自固定,请参见固定艺术)。如果没有收到OOM错误,或者需要确认是否是导致此问题的内存泄漏,那么唯一的方法就是分析应用程序。

我还将尝试确保以下几点:

a)实现Idisposable的所有内容都可以通过使用finally块或using语句(包括画笔,钢笔等)进行处理(有些人争辩说,将所有内容都设置为空)。

b)使用finally或using语句再次关闭具有close方法的任何内容(尽管我发现using并不总是关闭,具体取决于您是否在using语句之外声明了对象)

c)如果您使用的是非托管代码/ Windows API,则这些代码/窗口API将在之后正确处理。(有些有清理方法来释放资源)

希望这可以帮助。


19

如果您需要诊断.NET中的内存泄漏,请检查以下链接:

http://msdn.microsoft.com/zh-CN/magazine/cc163833.aspx

http://msdn.microsoft.com/zh-CN/magazine/cc164138.aspx

这些文章描述了如何创建进程的内存转储以及如何对其进行分析,以便您可以首先确定泄漏是不受管理的还是受管理的,以及是否可以管理泄漏,以及如何确定泄漏的来源。

微软还有一个更新的工具来协助生成崩溃转储,以代替ADPlus,称为DebugDiag。

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



15

关于垃圾收集器如何工作的最好解释是通过C#书(第20章)在Jeff Richters CLR中。阅读本文为理解对象如何持久化提供了良好的基础。

导致对象意外扎根的最常见原因之一是通过连接超出类的事件。如果您挂了一个外部事件

例如

SomeExternalClass.Changed += new EventHandler(HandleIt);

并在处理时忘记解开它,那么SomeExternalClass会引用您的类。

如上所述,SciTech内存分析器非常适合向您显示怀疑泄漏的对象的根源。

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

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

现在,做一些您认为会处理该类型对象的事情(例如,关闭窗口)。在这里有一个调试按钮可以运行的地方很方便System.GC.Collect()几次。

然后!dumpheap -stat -type <TypeName>再次运行。如果这个数字没有下降,或者没有下降到您期望的水平,那么您就有了进行进一步调查的基础。(我从Ingo Rammer的研讨会上获得了这个技巧)。


14

我猜在托管环境中,泄漏就是您不必要地引用周围的大量内存。


11

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

内存泄漏是当您附加到资源并且不让它消失时。您可以在托管和非托管编码中执行此操作。

关于.NET和其他编程工具,已经有关于垃圾收集的想法,以及其他使情况最小化的方法,这些情况会使您的应用程序泄漏。但是,防止内存泄漏的最好方法是,您需要了解所使用平台上的基础内存模型以及其工作方式。

相信GC和其他魔术可以清除混乱,这是内存泄漏的捷径,以后很难找到。

在对非托管代码进行编码时,通常会确保清理,您知道自己掌握的资源将是清理的责任,而不是看门人的责任。

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

用管理的术语讲,一旦进程被终止/删除,它确实会消失。

我看到很多人都这样做了,我真的希望这会结束。您不能要求用户终止您的应用程序以清理混乱!看一下可以是IE,FF等的浏览器,然后打开例如Google Reader,让它停留几天,然后看看会发生什么。

如果您随后在浏览器中打开另一个选项卡,浏览到某个站点,然后关闭承载导致浏览器泄漏的另一个页面的选项卡,您是否认为浏览器会释放内存?IE并非如此。如果我使用Google阅读器,则IE将在很短的时间内(大约3-4天)轻松占用1 GiB的内存。一些新闻页甚至更糟。


10

我猜在托管环境中,泄漏是您保留不必要的对周围大块内存的引用。

绝对。另外,在适当的情况下不对一次性对象使用.Dispose()方法可能会导致内存泄漏。最简单的方法是使用using块,因为它最后自动执行.Dispose():

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

而且,如果您创建的类使用的是非托管对象,那么如果您未正确实现IDisposable,则可能导致类用户的内存泄漏。


9

通过程序终止解决所有内存泄漏。

内存不足,操作系统可能会代表您决定解决此问题。


8

对于.net中的内存泄漏,我将与Bernard保持一致。

您可以分析应用程序以查看其内存使用情况,并确定是否在不应该使用的情况下管理大量内存,则可以说它存在泄漏。

用管理的术语讲,一旦进程被终止/删除,它确实会消失。

非托管代码本身就是野兽,如果其中存在泄漏,它将遵循标准的内存。泄漏定义。


7

还请记住,.NET有两个堆,一个是大对象堆。我相信大约85k或更大的对象放在此堆上。该堆的生存期规则与常规堆不同。

如果要创建大型内存结构(词典或列表的内存),则应谨慎查找确切的规则。

至于在进程终止时回收内存,除非您运行的是Win98或同等版本,否则所有内容都会在终止时释放回OS。唯一的例外是跨进程打开的事物,而另一个进程仍具有打开的资源。

COM对象可能很棘手。如果您始终使用该IDispose模式,则将是安全的。但是我遇到了一些实现的互操作程序集IDispose。完成后,这里的关键是打电话Marshal.ReleaseCOMObject。COM对象仍使用标准COM参考计数。



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.