C# 中的内存泄漏

Posted

技术标签:

【中文标题】C# 中的内存泄漏【英文标题】:Memory Leak in C# 【发布时间】:2010-10-11 20:49:06 【问题描述】:

当您确保所有句柄、实现IDispose 的东西都被释放时,托管系统中是否有可能泄漏内存?

会不会有一些变量被遗漏的情况?

【问题讨论】:

另外,我看到 IDisposable 在人们编写的类上没有正确实现。它确实发生了。当然是真正的边缘案例,您可能只是在谈论使用框架中已有的东西。 可能,据我所知,一旦不再引用该对象,就会处置该对象。如果两个对象相互引用而您忘记从其中一个对象中删除引用怎么办。 @Noypi Gilas:这是一个常见的错误假设。据我所知,垃圾收集器从不 dispose 任何东西,他们甚至不知道 IDisposable 接口。垃圾收集器只知道当对象无法访问时它们调用的终结器(并且相应的终结器不会被抑制)。 【参考方案1】:

我认为 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;
    

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

【讨论】:

我也听说过这种叫做“核心癌”的东西——它会无限生长。 这个例子并不像你想象的那么愚蠢。 SharePoint 对象模型正是存在这个问题。我怀疑它们正在缓存必须从后备存储中检索的对象,但如果您访问许多此类对象,您会发现自己在相对较短的时间内内存不足。 是的,这不是一个愚蠢的例子。我的代码几乎就是这样,其中 FileSystemWatcher 对象创建一次,然后存储在静态集合中,直到程序关闭。他们所做的只是监视一些文件的变化。 在这个例子中泄露了什么?是 List 还是 var 的东西或两者兼而有之?我假设列表,但我不确定。 引用被“泄露”到列表中,因此这些对象永远不会被释放。【参考方案2】:

一旦对对象的所有引用都消失了,垃圾收集器将在下一次传递时释放该对象。我不会说内存泄漏是不可能的,但它相当困难,为了泄漏,你必须在没有意识到的情况下引用一个坐在周围的对象。

例如,如果您将对象实例化为一个列表,然后在完成后忘记将它们从列表中删除并且忘记释放它们。

【讨论】:

但是当列表超出范围时,应该清除列表中的所有项目吗?或者即使引用了其中一个元素,列表是否仍然存在? 在这种情况下,我假设列表是一个类的成员,因此被保留。你是对的,如果列表超出范围,它应该清除那些引用。【参考方案3】:

虽然框架中的某些东西可能存在泄漏,但更有可能的是您有一些东西没有被正确处理,或者某些东西阻止了 GC 处理它,IIS 将是这个问题的主要候选者.

请记住,并非 .NET 中的所有内容都是完全托管的代码、COM 互操作、文件 io(如文件流、数据库请求、图像等)。

我们不久前遇到的一个问题(IIS 6 上的 .net 2.0)是我们将创建一个映像然后将其处理掉,但 IIS 暂时不会释放内存。

【讨论】:

IIS - Internet Information Server - Microsoft 的网络服务器/网络服务平台。【参考方案4】:

如果未正确清理非托管资源,则可能会发生泄漏。实现IDisposable 的类可能会泄漏。

但是,常规对象引用不像低级语言那样需要显式内存管理。

【讨论】:

这绝对是目录服务对象的问题【参考方案5】:

唯一的泄漏(除了运行时中可能存在的错误,尽管由于垃圾收集而不太可能)将用于本机资源。如果您 P/Invoke 到本机库中,该库代表您的托管应用程序打开文件句柄或套接字连接或任何内容,并且您从未显式关闭它们(并且不在处置器或析构器/终结器中处理它们),您可以有内存或资源泄漏,因为运行时无法为您自动管理所有这些。

不过,如果您坚持使用纯托管资源,应该没问题。如果您在没有调用本机代码的情况下遇到任何形式的内存泄漏,那么这就是一个错误。

【讨论】:

没有人能阻止您创建大量程序不使用的对象。当它们在某个静态字段中被引用时,它们不会自动超出范围,它们必须显式地从对象图中“断开”。【参考方案6】:

如前所述,保留引用将导致内存使用量随着时间的推移而增加。进入这种情况的一个简单方法是使用事件。如果您有一个长寿命的对象,并且您的其他对象侦听了某个事件,如果从不删除侦听器,那么长寿命对象上的事件将在不再需要这些其他实例后很长时间内保持它们的活性。

【讨论】:

【参考方案7】:

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

【讨论】:

今天仍然如此——只有 DynamicMethods 在 AppDomain 处于活动状态时得到 GC。 @Curt:很高兴知道。直到知道我以为即使DynamicMethods 也会在 AppDomain 中持续存在。【参考方案8】:

委托可能会导致不直观的内存泄漏。

每当您从实例方法创建委托时,对该实例的引用都会存储在该委托“中”。

此外,如果您将多个委托组合成一个多播委托,那么只要在某处使用该多播委托,您就会拥有对众多对象的一大堆引用,这些对象不会被垃圾回收。

【讨论】:

你有什么文章可以进一步解释这一点吗?【参考方案9】:

.NET 应用程序中内存泄漏的唯一原因是对象仍在被引用,尽管它们的生命周期已经结束。因此,垃圾收集器无法收集它们。它们变成了长寿命的物体。

我发现在对象生命周期结束时订阅事件而不取消订阅很容易导致泄漏。

【讨论】:

这并不完全正确。阻塞终结器将阻止所有剩余的可终结对象被垃圾收集器回收。 IE。它们将以等待最终确定的内部对象列表为根。 具有终结器的对象在终结队列中等待其终结器被调用。【参考方案10】:

事件处理程序是非明显内存泄漏的常见来源。如果您从 object2 订阅 object1 上的事件,然后执行 object2.Dispose() 并假装它不存在(并从您的代码中删除所有引用),则 object1 的事件中有一个隐式引用将阻止 object2垃圾收集。

MyType object2 = new MyType();

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

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

object2.Dispose();

这是一种常见的泄漏情况 - 忘记轻松取消订阅事件。当然,如果 object1 被收集,object2 也会被收集,但直到那时。

【讨论】:

我认为这与代表的问题有关。 @JesonMartajaya 在那里评论 - 这是一个不同的(非)问题。你的例子和这个不一样。【参考方案11】:

正如其他人指出的那样,只要内存管理器中没有实际的错误,不使用非托管资源的类就不会泄漏内存。

您在 .NET 中看到的不是内存泄漏,而是永远不会被释放的对象。只要垃圾收集器可以在对象图上找到一个对象,它就不会被释放。因此,如果任何地方的任何活对象都引用了该对象,它就不会被释放。

事件注册是实现这一目标的好方法。如果一个对象注册了一个事件,它所注册的任何东西都有对它的引用,即使你消除了对该对象的所有其他引用,直到它取消注册(或者它注册的对象变得不可访问)它都会保持活动状态。

因此,您必须注意在您不知情的情况下注册静态事件的对象。例如,ToolStrip 的一个漂亮功能是,如果您更改显示主题,它将自动在新主题中重新显示。它通过注册静态SystemEvents.UserPreferenceChanged 事件来完成这个漂亮的功能。当您更改 Windows 主题时,会引发该事件,并且所有正在侦听该事件的 ToolStrip 对象都会收到新主题的通知。

好的,假设您决定丢弃表单上的ToolStrip

private void DiscardMyToolstrip()

    Controls.Remove("MyToolStrip");

你现在有一个永远不会死的ToolStrip。即使它不再出现在您的表单上,每次用户更改主题时,Windows 都会尽职尽责地告诉原本不存在的ToolStrip。每次垃圾收集器运行时,它都会认为“我不能把那个对象扔掉,UserPreferenceChanged 事件正在使用它。”

这不是内存泄漏。但也有可能。

这样的事情使内存分析器变得非常宝贵。运行内存分析器,你会说“这很奇怪,堆上似乎有一万个 ToolStrip 对象,尽管我的表单上只有一个。这是怎么发生的?”

哦,如果您想知道为什么有些人认为属性设置者是邪恶的:要让 ToolStripUserPreferenceChanged 事件中注销,请将其 Visible 属性设置为 false

【讨论】:

【参考方案12】:

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

【讨论】:

【参考方案13】:

在托管代码中不能泄漏内存是一个神话。诚然,这比非托管 C++ 中要困难得多,但有上百万种方法可以做到这一点。持有引用、不必要的引用、缓存等的静态对象。如果你以“正确”的方式做事,那么你的许多对象直到比必要的更晚才会被垃圾收集,这在我看来也是一种内存泄漏,以实际而非理论的方式。

幸运的是,有一些工具可以帮助您。我经常使用微软的CLR Profiler——它不是有史以来最友好的工具,但它绝对非常有用,而且是免费的。

【讨论】:

【参考方案14】:

您可能会发现我的新文章很有用:How to detect and avoid memory and resources leaks in .NET applications

【讨论】:

【参考方案15】:

在我的上一份工作中,我们使用了一个像筛子一样泄漏的第三方 .NET SQLite 库。

我们在每次都必须打开和关闭数据库连接的奇怪情况下进行了大量快速数据插入。第 3 方库做了一些我们应该手动进行的相同连接打开,但没有记录它。它还在我们从未找到的地方保存了参考资料。结果是打开的连接数量是预期的 2 倍,而只有 1/2 的连接被关闭。由于引用被保留,我们有内存泄漏。

这显然与经典的 C/C++ 内存泄漏不同,但从所有意图和目的来看,它对我们来说都是一个。

【讨论】:

请问是哪个库? @Nik:不记得是哪个库,但自 2008 年以来我就没有在那家公司工作过,所以我敢肯定这是某个不再存在或已修复的库。 你应该举个例子。【参考方案16】:

并不是真正的内存泄漏,但在使用大对象(如果我没记错的话,大于 64K)时很容易耗尽内存。它们存储在 LOH 上,并且没有进行碎片整理。因此,使用这些大对象并释放它们会释放 LOH 上的内存,但 .NET 运行时不再为该进程使用该空闲内存。因此,只需使用 LOH 上的几个大对象,您就可以轻松地用完 LOH 上的空间。微软知道这个问题,但我记得现在正在计划解决这个问题。

【讨论】:

LOH 最终会进行碎片整理,但这不是确定性的,可能需要一段时间。【参考方案17】:

如果考虑内存泄漏,也可以用这种代码实现:

public class A

    B b;
    public A(B b)  this.b = b; 
    ~A()
    
        b = new B();
    


public class B

    A a;
    public B()  this.a = new A(this); 
    ~B()
    
        a = new A(this);
    


class Program

    static void Main(string[] args)
    
        
            B[] toBeLost = new B[100000000];
            foreach (var c in toBeLost)
            
                toBeLost.ToString(); //to make JIT compiler run the instantiation above
            
        
        Console.ReadLine();
    

【讨论】:

您应该添加一个解释,说明这如何以及为什么会导致内存泄漏。否则人们只需要相信它。【参考方案18】:

小函数有助于避免“内存泄漏”。因为垃圾收集器在函数结束时释放局部变量。如果函数很大并且占用大量内存,您必须自己释放占用大量内存且不再需要的局部变量。类似的全局变量(数组、列表)也很糟糕。

我在创建图像而不是处理图像时遇到了 C# 中的内存泄漏。这有点奇怪。人们说你必须在每个拥有它的对象上调用 .Dispose() 。但是图形 C# 函数的文档并不总是提到这一点,例如函数 GetThumbnailImage()。我认为 C# 编译器应该警告你。

【讨论】:

【参考方案19】:

自我提醒 如何找到内存泄漏:

删除和 gc.collect 调用。 等到我们确定内存正在泄漏。 从任务管理器创建转储文件。 使用DebugDiag打开转储文件。 首先分析结果。那里的结果应该可以帮助我们,这个问题通常会占用大部分内存。 修复代码,直到找不到内存泄漏为止。 使用 .net profiler 等第 3 方应用程序。 (我们可以试用,但需要尽快解决问题。第一次转储应该主要帮助我们了解泄漏的原因) 如果问题出在虚拟内存中,则需要观察非托管内存。 (通常那里有另一个配置需要启用) 根据使用方式运行第 3 方应用程序。

常见的内存泄漏问题:

永远不会删除事件/委托。 (处置时,请确保该事件未注册) - 请参阅ReepChopsey 答案 列表/字典从未被清除。 引用保存在内存中的另一个对象的对象永远不会被释放。 (克隆它以便于管理)

【讨论】:

【参考方案20】:

在控制台或 Win 应用程序中创建一个 Panel 对象(panel1),然后添加 1000 个 PictureBox 并设置其 Image 属性,然后调用 panel1.Controls.Clear。所有 PictureBox 控件仍在内存中,GC 无法收集它们:

var panel1 = new Panel();
var image = Image.FromFile("image/heavy.png");
for(var i = 0; i < 1000;++i)
  panel1.Controls.Add(new PictureBox()Image = image);

panel1.Controls.Clear(); // => Memory Leak!

正确的做法是

for (int i = panel1.Controls.Count-1; i >= 0; --i)
   panel1.Controls[i].Dispose();

Memory leaks in calling Controls.Clear()

调用 Clear 方法不会从内存中删除控制句柄。 您必须显式调用 Dispose 方法以避免内存泄漏

【讨论】:

【参考方案21】:

使用 .NET XmlSerializer 时可能会发生内存泄漏,因为它使用了不会被释放的非托管代码。

查看文档并在此页面上搜索“内存泄漏”:

https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializer?view=netframework-4.7.2

【讨论】:

以上是关于C# 中的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

PerfView专题 (第三篇):如何寻找 C# 中的 VirtualAlloc 内存泄漏

为啥使用“新”会导致内存泄漏?

PerfView专题 (第二篇):如何寻找 C# 中的 Heap堆内存泄漏

堆中的 DLL 内存泄漏

使用 C# 的图像加载内存泄漏

哪些策略和工具可用于查找 .NET 中的内存泄漏?