C# 的内存泄漏

50

我想更好地理解内存泄漏的概念。有没有人能够提供一些有用的信息,帮助我更好地理解什么是内存泄漏以及如何在我的代码中找到它们。


2
以下是我之前写的一篇关于 .Net 内存泄漏的博客文章链接:http://crazorsharp.blogspot.com/2009/03/net-memory-leaks-it-is-possible.html - BFree
Windows注册表 → 搜索 → “HeapLeakDetection” - Bitterblue
24
这怎么可能是离题的呢?他想了解内存泄漏,这是可以直接回答并且直接相关的,在我谦虚但正确的观点中。 - Tim Long
4
有点晚了...我同意这不是离题讨论,就我个人而言。 - Nirman
5
有人投票删除这个问题让我感到担忧。虽然可能有几个原因导致此问题与主题不符,但删除它是个坏主意。它包含了有用的信息。 - Liam
显示剩余2条评论
6个回答

53

内存泄漏有很多种,但通常指某种不再使用但仍占用内存的资源。如果这些问题过多,应用程序将占用大量内存并最终耗尽。

在C#中,以下是一些常见的内存泄漏:

  • 未移除事件监听器。任何使用匿名方法或lambda表达式创建的事件监听器引用外部对象都会使这些对象保持活动状态。记得在不再使用时删除事件监听器。
  • 保持未使用的数据库连接或结果集处于打开状态。记得对所有IDisposable对象调用Dispose()使用using语句
  • 调用使用p/Invoke的C函数分配内存,然后从未释放。

43
传统的内存泄漏发生在你分配了内存,然后不知何故“忘记”释放或释放它。在旧的C++代码中,这意味着调用new而没有对应的delete。在C语言中,这意味着调用alloc()/malloc()而没有对应的free()
在.NET中,您不会按传统意义上遇到内存泄漏问题,因为您不需要自己释放内存。没有相当于free()delete需要使用的函数。即使是IDisposable和终结器也不是关于内存的。取而代之的是,您依靠垃圾收集器(GC)来为您释放内存。
但是,这并不意味着您永远不会失去内存跟踪。有几种方式可能会无意中保留引用,从而防止垃圾回收器完成其工作。这些包括全局变量(尤其是列表、字典和其他可能用于“缓存”对象的集合类型)、事件处理程序挂起对象引用、递归历史引用和大对象堆。
还要注意一点,在.NET中,内存使用量增加的模式不一定意味着应用程序存在内存泄漏。在总体内存压力较低的情况下,垃圾收集器可以选择节省时间而不进行回收,或者仅在进程的现有地址空间中回收而不将内存返回给操作系统。

3
“旧版C++”是否指没有智能指针的C++? - user666412

17
非常好的一篇文章是“每个人都错误地思考了垃圾回收”
通常,内存泄漏或任何资源泄漏都是指程序在分配内存(或任何其他资源)后,在完成使用后未释放它。在本机应用程序中,内存泄漏是最常见的资源泄漏,并且可能会发生在资源引用(指向已分配块的指针)超出范围并被销毁时,但分配的资源(内存块)未被销毁的情况下。在这种情况下,资源(内存)被泄漏,因为程序已经失去了释放它的能力,即使它想要释放,因为它不再记得资源的位置(块的地址)。
在托管应用程序中,内存泄漏有点棘手。由于运行时可以自动跟踪对资源的引用,因此它还可以理解当应用程序不再引用某个资源(对象)时(从堆栈帧到该资源的任何线程上没有引用链),因此运行时可以理解何时安全地回收应用程序不再引用的对象。因此,在托管世界中,“泄漏”会发生在您认为应用程序不再引用对象(因此可以由运行时收集),但实际上,通过某些引用链,您确实对其有引用,因此无法收集。
我强烈推荐上面链接的Raymond Chen的文章,非常地有启发性。

2
更新的 Raymond Chen 链接:https://devblogs.microsoft.com/oldnewthing/20100809-00/?p=13203 - Bob Horn

10
当内存被分配给一个应用程序时,该应用程序有责任释放该内存以便操作系统可以重新分配给其他应用程序使用。当应用程序没有释放该内存时,内存泄漏就会发生,从而防止其被重新分配。
对于托管代码,垃圾收集器会跟踪应用程序创建的对象的引用。对于大多数情况下,CLR将代表运行进程透明和合理地处理内存分配和释放。但是,.NET开发人员仍需要考虑资源管理,因为尽管垃圾收集器在工作,但仍存在内存泄漏的情况。
考虑以下代码: Widget widget = new Widget(); 上面的代码行创建了Widget类的新实例,并将widget字段分配为对该对象的引用。GC跟踪与每个对象关联的引用,并释放没有强引用的对象的内存。
值得一提的是,CLR的垃圾收集器只会收集托管对象,.NET代码可以并且经常使用无法自动进行垃圾回收的非托管资源。
当为那些资源分配了内存的对象在最后一个对这些资源的引用超出范围之前未能正确地将它们释放时,会发生非托管资源泄漏,这意味着资源被分配,但对应用程序不可用。
直接引用非托管资源的类应确保这些资源被正确地释放。下面是一个示例:
public void ManagedObject : IDisposable
{
    //A handle to some native resource.
    int* handle;

    public ManagedObject()
    {
        //AllocateHandle is a native method called via P/Invoke.
        handle = AllocateHandle();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            //deal with managed resources here
            FreeHandle(handle);
        }
    }

    ~ManagedType()
    {
        Dispose(false);
    }
}
disposing参数在从finalizer被调用时为false。这是为了防止在finalizer中使用托管资源,因为在那个阶段托管引用应该被视为无效。还请注意,Dispose()方法调用GC.SuppressFinalize(this),以防止针对该实例运行finalizer。这是因为会在Dispose调用中释放在finalizer中将要被释放的资源,从而使调用finalizer不必要。使用处理非托管资源的类(或任何实现IDisposable的类)的客户端代码应在using块内进行,以确保在不再需要访问资源时调用IDisposable.Dispose,这将处理托管和非托管资源,并且在上面的示例中,可以确保不会进行非常昂贵的finalizer调用。对我的啰嗦道歉,我停止了。

4

内存泄漏是指程序在动态分配内存后,在使用完毕后没有正确释放。如果你的程序一直这样做,内存泄漏会越来越大,很快你的程序就会占用所有的RAM。


4
"内存泄漏"应该被定义为"在您认为不应该使用内存时使用的内存",以适用于C#/Java等垃圾收集语言/运行时。传统上,“内存泄漏”被定义为未正确释放的内存(请参见其他答案中的维基百科链接),这通常不会发生在垃圾收集环境中。需要注意的是,由于运行时问题,即使是垃圾收集语言也可能会出现内存泄漏——例如,即使JavaScript是一种垃圾收集语言,在Internet Explorer的JavaScript运行时中仍很容易泄漏大量JavaScript对象。

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