C#中的内存泄漏问题

56

在托管系统中,当您确保所有句柄(实现 IDispose 接口的对象)都已释放时,是否可能出现内存泄漏?

是否存在一些变量被遗漏的情况?


此外,我曾经看到人们编写的类没有正确实现IDisposable接口。这确实会发生。当然,这是一个真正的边缘情况,你可能只是在谈论已经在框架中使用的东西。 - core
据我所知,当一个对象没有被引用时,它就会被处理掉。但是如果两个对象相互引用,并且你忘记从其中一个对象中删除引用,那会怎么样呢? - sss
@Noypi Gilas:这是一个常见的错误假设。据我所知,垃圾收集器从不“处理”任何东西,它们甚至不知道IDisposable接口。垃圾收集器只知道终结器,当对象变得不可访问时(并且相应的终结器未被抑制),它们会调用终结器。 - Christoph
21个回答

65

事件处理程序是非常普遍的隐蔽内存泄漏来源。如果你在对象1上订阅了来自对象2的事件,然后执行object2.Dispose()并假装它不存在(从代码中删除所有引用),那么在对象1的事件中有一个隐含的引用将防止对象2被垃圾回收。

MyType object2 = new MyType();

// ...
object1.SomeEvent += object2.myEventHandler;
// ...

// Should call this
// object1.SomeEvent -= object2.myEventHandler;

object2.Dispose();

这是一个常见的泄漏案例——容易忘记取消订阅事件。当然,如果object1被回收了,object2也会被回收,但要等到那时才行。


我认为这与委托问题有关。 - AndersK
1
@JesonMartajaya 在那里发表评论-这是一个不同的(非)问题。你的例子和这个不一样。 - Reed Copsey

41

我认为C++风格的内存泄漏是不可能的。垃圾回收器应该能够处理这些情况。但是,可能会创建一个静态对象,即使这些对象再也没有被使用,该对象仍然聚合了对象引用。例如:

public static class SomethingFactory
{
    private static List<Something> listOfSomethings = new List<Something>();

    public static Something CreateSomething()
    {
        var something = new Something();
        listOfSomethings.Add(something);
        return something;
    }
}

那是一个显然很愚蠢的例子,但它相当于托管运行时的内存泄漏。


我也听说过这种东西被称为“核心癌症”——它无限制地增长。 - Jeffrey Hantin
7
这个例子并不像你想象中的那么愚蠢。SharePoint对象模型恰好存在这个问题。我猜他们正在缓存必须从后端存储中检索的对象,但如果你访问许多这样的对象,你会发现自己的内存很快就用完了。 - Ben Collins
1
引用被“泄漏”到列表中,因此这些对象永远不会被处理。 - RCIX
我写了一个控制台程序来演示这个泄漏:https://gist.github.com/tbowers/cea8721a24e857bd8e78。每次按键时,都会创建另一个“Something”,并且您会看到分配了约40mb的内存。我不理解的是,即使我删除此`List<>部分,我仍然看不到在每个while`迭代之后回收的内存。难道只是等待GC进行收集吗? - Ternary
@Ternary,这可能是因为您的对象已经通过第一个代回收周期。请阅读http://msdn.microsoft.com/en-us/library/ee787088(v=vs.110).aspx,特别注意“垃圾回收的条件”和“代”的部分。 - Michael Meadows
显示剩余2条评论

28
正如其他人所指出的那样,只要内存管理器中没有实际 bug,不使用非托管资源的类就不会泄漏内存。在 .NET 中看到的不是内存泄漏,而是从未被处理的对象。只要垃圾回收器能在对象图中找到它,对象就不会被处理。因此,如果任何活动对象都引用了该对象,则它就不会被处理。
事件注册是导致这种情况发生的一种好方法。如果一个对象注册了一个事件,任何它注册过的对象都有对它的引用,即使您消除了对象的所有其他引用,只要它不取消注册(或者注册它的对象变得不可访问),它就会保持活动状态。因此,您必须注意那些未经您知道就注册了静态事件的对象。例如,ToolStrip 的一个巧妙之处在于,如果更改显示主题,它将自动以新主题重新显示。它通过注册静态 SystemEvents.UserPreferenceChanged 事件来完成此巧妙功能。当您更改 Windows 主题时,会引发事件,并通知所有正在侦听事件的 ToolStrip 对象,说明有一个新的主题。
好的,假设您决定放弃在表单上的一个 ToolStrip:
private void DiscardMyToolstrip()
{
    Controls.Remove("MyToolStrip");
}

你现在有一个永远不会消失的ToolStrip。即使它不再在你的表单上,每次用户更改主题时,Windows都会如实地告诉这个看起来不存在的ToolStrip。每次垃圾回收器运行时,它都会认为:“我不能扔掉那个对象,因为UserPreferenceChanged事件正在使用它。”

这不是内存泄漏。但也可以说是。

像这样的问题使得内存分析器非常有价值。运行内存分析器,你会发现“奇怪的是,似乎在堆中有一万个ToolStrip对象,尽管我的表单上只有一个。这是怎么回事?”

哦,如果你想知道为什么有些人认为属性设置器很邪恶:要让ToolStripUserPreferenceChanged事件注销,将其Visible属性设置为false


21

委托可能会导致令人费解的内存泄漏。

每当从实例方法创建委托时,一个指向该实例的引用就被存储“在”该委托中。

此外,如果将多个委托合并成多路广播委托,则将具有对众多对象的引用的大块引用保留,只要该多路广播委托在某个地方被使用,这些对象就不会被垃圾回收。


13
你有没有更多解释这个问题的文章? - Chris Marisic

17
如果您正在开发WinForms应用程序,一个微妙的“泄漏”是Control.AllowDrop属性(用于启用拖放)。如果将AllowDrop设置为“true”,CLR仍将通过System.Windows.Forms.DropTarget保留您的控件。要解决这个问题,请确保当您不再需要它时,您的ControlAllowDrop属性设置为false,CLR会处理余下的事情。

8
在.NET应用程序中,内存泄漏的唯一原因是尽管对象的生命周期已经结束,但仍然被引用。因此,垃圾回收器无法收集它们,它们变成了长期存在的对象。
我发现,通过在对象的生命周期结束时不取消订阅事件,很容易导致内存泄漏。

这并不完全正确。一个阻塞的 finalizer 会阻止所有剩余的可终结对象被垃圾回收器回收。也就是说,它们将被内部等待终结对象列表所引用。 - Brian Rasmussen
1
等待调用其终结器的终结器队列中具有终结器的对象仍在被引用。 - tranmq

7

如前所述,保留引用会导致随着时间的推移内存使用量增加。一种容易陷入这种情况的方法是使用事件。如果您有一个长期存在的对象,其中包含一些其他对象监听的事件,如果这些监听器从未被移除,那么长寿命对象上的事件将使得这些其他实例在不再需要它们后仍然保持活动状态。


6

5

反射发射是另一个潜在的泄漏源,例如内置对象反序列化器和花哨的SOAP / XML客户端。至少在框架的早期版本中,依赖AppDomains中生成的代码从未被卸载...


1
今天仍然是正确的 -- 只有在 AppDomain 存在时才会回收 DynamicMethods。 - Curt Hagenlocher
@Curt:好知道。直到现在我还以为即使是DynamicMethod也会在AppDomain中持久存在。 - Konrad Rudolph

4
这是一个错误认识,认为在托管代码中无法泄漏内存。虽然比在非托管 C++ 中更难,但有许多方法可以做到。静态对象持有引用、不必要的引用、缓存等等。如果你按照“正确”的方式进行操作,很多对象直到比必要的时间晚很多才会被垃圾回收,这在我的看法中也是一种内存泄漏,从实际和非理论的角度来看。
幸运的是,有一些工具可以帮助你。我经常使用 Microsoft 的 CLR Profiler - 它并不是编写过的最用户友好的工具,但它绝对非常有用,而且是免费的。

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