.NET中的内存泄漏问题

20

在.NET中,可能会出现哪些导致内存泄漏的方式?

我知道其中两种:

  1. 没有正确取消注册事件处理程序/委托
  2. 在Windows Forms中未释放动态子控件:

示例:

// Causes Leaks  
Label label = new Label();  
this.Controls.Add(label);  
this.Controls.Remove(label);  

// Correct Code  
Label label = new Label();  
this.Controls.Add(label);  
this.Controls.Remove(label);  
label.Dispose();
更新:这个想法是列出一些常见的陷阱,这些陷阱并不太明显(例如上面的例子)。通常人们认为内存泄漏不是一个大问题,因为有垃圾回收器。这和C++不同。

大家讨论得很好,但让我澄清一下……按照定义,在.NET中,如果没有对一个对象的引用,它将在某个时刻被垃圾回收。因此,这不是诱发内存泄漏的方法。

在托管环境中,如果您对任何未意识到的对象有意外引用(因此在我的问题中有两个示例),我会认为这是内存泄漏。

那么,可能导致这种内存泄漏的各种可能方式是什么?


2
正如Keith所说,你的示例不会导致内存泄漏。 - tobsen
14个回答

21

这并不会真正地导致泄漏,只是会增加垃圾回收器的工作量:

// slows GC
Label label = new Label();  
this.Controls.Add(label);  
this.Controls.Remove(label);  

// better  
Label label = new Label();  
this.Controls.Add(label);  
this.Controls.Remove(label);  
label.Dispose();

// best
using( Label label = new Label() )
{ 
    this.Controls.Add(label);  
    this.Controls.Remove(label);  
}

在像 .Net 这样的托管环境中,随意留下一次性组件并不是什么大问题 - 这正是托管的重要部分。

当然,这会使您的应用程序变慢。但您不会为其他任何东西留下混乱。


1
@Keith,最后一个方法不幸地行不通(因为添加和删除通常不在同一位置 - 我只是展示了问题)。此外,它不仅会减慢垃圾回收的速度,而且根据表单的复杂性,您很容易使应用程序崩溃。 - Vaibhav
1
把控件随意丢在那里是非常危险的。对于小型的一次性应用程序来说,这样做可能还可以,但在真正的应用程序中,你应该始终处理好它们,否则你会后悔的。相信我,我曾经有过这样的经历。 - Niki
1
我遇到了这个问题,这是一件危险的事情——离开可以丢弃的东西!为了测试这个问题,可以在循环中创建和删除System.Drawing.Bitmap——你很快会得到OutOfMemoryException错误,垃圾回收也无法帮助解决。 - bohdan_trotsenko
1
@modosansreves - 是的,那肯定会破坏你的应用程序。但是,由于你得到了一个托管的 OutOfMemoryException,你只会崩溃这个应用程序。而未经管理的内存泄漏将导致蓝屏、挂起或整个机器崩溃。 - Keith

14

在不使用BindingSource类的实例直接设置GridControl.DataSource属性(http://msdn.microsoft.com/en-us/library/system.windows.forms.bindingsource.aspx)会导致我的应用程序发生泄漏,我花了很长时间才用分析器追踪到这个问题。最终我找到了微软回应的这个错误报告:http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=92260

有趣的是,在BindingSource类的文档中,微软试图将其打造成一个合法的深思熟虑的类,但我认为他们只是创建它来解决关于货币管理器和将数据绑定到网格控件的基本泄漏问题。

小心这一点,我敢打赌有许多泄漏的应用程序都是因为这个问题造成的!


7
非常好的文章。我们有一个移动应用程序,我们一直在努力清理资源,并确保处理所有实现IDisposable的内容。即使经过所有这些努力,我们在大量使用后仍然会在现场遇到偶发崩溃。我们正好面对了这种情况!你很棒。 - Mat Nadrofsky
1
Mat Nadrofsky的评论加1,哈哈 - Drevak

14

无法提供全面的列表,这有点像问“你如何弄湿?”

尽管如此,请确保对实现IDisposable接口的所有内容都调用Dispose(),并确保在任何消耗任何类型非托管资源的类型上实现IDisposable。

不时地,在您的代码库中运行像FxCop这样的工具,帮助您执行该规则-您会惊讶于一些可处理对象在应用程序框架中被深埋的程度。


你会如何设置 FxCop 来强制执行该规则? - Joel

5

11
那是什么意思? - Eric Nicholson
终结器是单线程的。当可以被处理的对象最终被处理时,就会发生“终结”。如果某个特定项无法被处理,则不会处理任何内容,最终导致内存耗尽。 - Leon Bambrick
那么在这个上下文中,“block”是什么意思——覆盖和代码包装终结器函数,还是完全阻止它运行? - Hardryv
4
好的,既然这被标记为答案,那请您用完整的句子等方式详细说明一下…… - Kieren Johnstone
我相信他的意思是在终结器中使用永远无法完成的代码 - 无限循环、死锁或类似的情况。 - Basic

4

Finalize(或来自终结器的Dispose调用)方法中的异常会阻止未托管的资源被正确释放。常见的错误是程序员 假设 对象将被释放的顺序,并尝试释放已被释放的对等对象,导致异常并导致剩余的Finalize/Dispose从Finalize方法未被调用。


3

我有四个额外的内容要添加到这个讨论中:

  1. 终止创建了 UI 控件而没有适当准备的线程 (Thread.Abort()) 可能会导致内存被意外使用。

  2. 通过 Pinvoke 访问非托管资源而不清理它们可能会导致内存泄漏。

  3. 修改大型字符串对象。虽然不一定会导致内存泄漏,但是一旦超出作用域,GC 会处理它,但在性能方面,如果经常修改大型字符串,您不能真正依赖于 GC 来确保程序的占用空间最小。

  4. 经常创建 GDI 对象以执行自定义绘图。如果经常进行 GDI 工作,请重用单个 GDI 对象。


2
为了防止 .NET 内存泄漏:
1)每当创建一个带有 'IDisposable' 接口的对象时,请使用“using”结构(或'try-finally'结构)。
2)如果类创建线程或将对象添加到静态或长寿命集合中,请使其成为 'IDisposable'。请记住,C# 'event' 是一个集合。
这里是一篇关于防止内存泄漏的小文章:Tips to Prevent Memory Leaks

2
你是在谈论意外的内存使用还是实际的内存泄漏?你列举的两种情况并不完全是泄漏,它们是指对象停留时间比预期更长的情况。
换句话说,它们是调用它们的人不知道或忘记的引用,被称为内存泄漏。
编辑:或者它们是垃圾回收器或非托管代码中的实际错误。
编辑2:另一种思考方式是始终确保对您的对象的外部引用得到适当释放。外部意味着超出您控制范围的代码。任何发生这种情况的情况都是可能“泄漏”内存的情况。

2
调用IDisposable是开始的最简单的地方,绝对是在代码库中获取所有低悬垂内存泄漏问题的有效方法。不过,这并不总是足够的。例如,了解在运行时生成托管代码的时间和方式以及一旦程序集加载到应用程序域中就永远不会被卸载,这可能会增加应用程序的内存占用。

2
不行。在 .Net 的 Compact Framework 上开发,如果您不正确处理对象的释放,您的设备很快就会耗尽内存,因此您不能真正地概括这一点。您不能等待垃圾回收器来完成它。 - Mat Nadrofsky

1

有一件对我来说非常意外的事情是:

Region oldClip = graphics.Clip;
using (Region newClip = new Region(...))
{
    graphics.Clip = newClip;
    // draw something
    graphics.Clip = oldClip;
}

内存泄漏在哪里?没错,你也应该处理掉 oldClip!因为Graphics.Clip是一种罕见的属性,每次调用getter时都会返回一个新的一次性对象。


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