为啥.NET 没有内存泄漏?

Posted

技术标签:

【中文标题】为啥.NET 没有内存泄漏?【英文标题】:Why can .NET not have memory leaks?为什么.NET 没有内存泄漏? 【发布时间】:2011-02-01 07:59:57 【问题描述】:

忽略不安全的代码,.NET 不会有内存泄漏。我从许多专家那里读了无数遍,我相信这一点。但是,我不明白为什么会这样。

据我了解,框架本身是用 C++ 编写的,而 C++ 容易出现内存泄漏。

底层框架写得这么好,绝对没有内存泄露的可能吗? 框架的代码中是否有一些东西可以自我管理甚至治愈它自己的潜在内存泄漏? 我还没有考虑其他答案吗?

【问题讨论】:

请向任何“专家”发布链接,指出 .NET 框架(或 any 语言中的 any 框架)不可能拥有内存泄漏。我需要给那个人写一封电子邮件。 @Shaggy Frog:请参阅下面的答案。这并不是说 .NET Framework 完美无缺或不会泄漏。该问题的答案取决于在框架源代码内部完成的经典内存分配/释放与在处理引用方面框架完成的内存管理之间的区别.这大概是总结的太简单了,但是下面有很多精彩更完整的解释。 我只是在阅读您目前所说的问题:“忽略不安全的代码,.NET 不会有内存泄漏。我从许多专家那里读到了无数遍,我相信这一点。”框架有得到修复的错误,这就是更新的原因。再说一次,我会说我有兴趣阅读其中一些“专家”意见,所以请发布此类链接。 【参考方案1】:

.NET 可能有内存泄漏。

大多数情况下,人们指的是垃圾收集器,它决定何时可以摆脱一个对象(或整个对象循环)。这避免了 classic c 和 c++ 风格的内存泄漏,我的意思是分配内存而不是稍后释放它。

但是,很多时候程序员没有意识到对象仍然有悬空引用并且没有被垃圾回收,从而导致...内存泄漏。

通常情况下,事件已注册(使用+=)但稍后未取消注册,但也适用于访问非托管代码(使用 pInvokes 或使用底层系统资源的对象,例如文件系统或数据库连接)和没有正确处理资源。

【讨论】:

例如:忘记处理操作系统小部件。 GC 比引用计数复杂得多,但除此之外是的。 C/C++ 中的内存泄漏是“未释放但不再引用的东西”。 Java/.NET 中的内存泄漏是“我们仍在引用但不应该引用的东西”。当然 .NET 类型的泄漏仍然可能在 C/C++ 中发生,因此在 .NET 中发生内存泄漏的可能性要小一些,但可能性仍然很大。 在高性能虚拟机中很少使用引用计数。对于多线程应用程序来说,开销是巨大的。【参考方案2】:

由于垃圾回收,您不会经常发生内存泄漏(除了不安全代码和 P/Invoke 等特殊情况)。但是,您当然可以无意中使引用永远保持活动状态,这实际上会泄漏内存。

编辑

到目前为止,我见过的真正泄漏的最佳示例是事件处理程序 += 错误。

编辑

请参阅下面的错误解释,以及在何种条件下它被认为是真正的泄漏而不是几乎真正的泄漏。

【讨论】:

您能解释一下“事件处理程序 += 错误”吗?原谅我的无知:) @Pwninstein。如果您使用 MyClass.CoolEvent += new EventHandler(HandleCoolEvent); 附加一个事件处理程序;这实际上将事件处理程序添加到内部列表。如果你想避免内存泄漏,你应该以同样的方式分离,但将 += 替换为 -=。这将从内部列表中删除事件处理程序。 补充一点,如果你不删除事件(或对象本身,或两者,我现在不记得)将永远存在,或者至少直到结束你的程序。 如果你不再有委托,我会说这只是一个“真正的”泄漏,所以即使你想你也不能-=它。如果您使用匿名方法,则可能会立即发生这种情况,或者如果您丢失对类实例的所有引用(但它仍然卡在内存中,当然)。 我想说,根据这个线程中流行的“真正的泄漏”的定义,+= 不符合条件。这是因为持有该事件的类可以在任何时候说MyEvent = null,从而删除引用并允许GC 释放内存。也就是说,引用本身仍然可用,尽管只对那个类。【参考方案3】:

在查看 Microsoft 文档后,特别是“Identifying Memory Leaks in the CLR”,Microsoft 确实声明,只要您不在应用程序中实现不安全代码,就不可能发生内存泄漏

现在,他们还指出了感知内存泄漏的概念,或者如 cmets 中所指出的“资源泄漏”,即使用具有延迟引用且未正确处理的对象。 IO 对象、数据集、GUI 元素等可能会发生这种情况。在使用 .NET 时,它们通常等同于“内存泄漏”,但它们不是传统意义上的泄漏。

【讨论】:

某些 .NET 类,例如您提到的那些,包装 Windows 内核对象(例如文件句柄),因此让它们退出范围而不调用 Dispose 会延迟底层资源的释放直到 GC 最终解决它。这种错误可能会使文件被锁定且无法访问,因此确实很糟糕,但这并不是真正的内存泄漏。 @Steven 在某种程度上是的......但实际上它是一个泄漏,你如何修复它。我想这取决于你对“泄漏”的定义 也许我很挑剔,但我对泄漏的看法是资源丢失永久(或至少在进程关闭之前)。事件错误是真正的泄漏,而这个例子只是一个不受欢迎的缓慢。同样,这可能是个好点,我肯定会立即解决这样的问题。 @Steven - 根据使用情况,它可能会丢失,直到进程关闭,我一直在文件、XMLSerializers 和代码中的其他对象中看到这一点。 句柄是 resources,未能释放(处置)它们会导致 resource 泄漏,但这通常不称为 memory 泄漏。【参考方案4】:

.NET 可能存在内存泄漏,但它可以帮助您避免它们。所有引用类型对象都从一个托管堆中分配,该堆跟踪当前正在使用的对象(值类型通常分配在堆栈上)。每当在 .NET 中创建新的引用类型对象时,都会从这个托管堆中分配它。垃圾收集器负责定期运行和释放任何不再使用的对象(不再被应用程序中的任何其他对象引用)。​​

Jeffrey Richter 的书 CLR via C# 有一个很好的章节介绍了如何在 .NET 中管理内存。

【讨论】:

这在技术上是不正确的:有些对象分配在堆栈上,而不是堆上。 正如 Steven Sudit 所指出的,进行了更正以区分堆栈与堆。 仍然在技术上不正确。声明类中的值类型在堆上分配。并且对引用类型对象的引用同样可以在堆或堆栈上,具体取决于。 @Steven 是对的:堆/堆栈是一个实现细节,在 C#/.NET 中编写代码时不应该关注它。唯一的例外是在编写 C++/CLI 时,它允许您显式选择堆或堆栈语义,后者是一种穷人的 RAII。在 C# 中,这并不适用,并且您永远不应假设特定对象将在堆或堆栈上分配,无论其类型如何。 -1。抱歉,这个答案中唯一解决内存泄漏问题的部分是您提到了垃圾收集器。您提到了堆栈和堆,但没有提到它们在内存泄漏或缺乏方面所起的作用。【参考方案5】:

嗯,.NET 有一个 garbage collector 在它认为合适的时候清理东西。这就是它与其他非托管语言的区别。

但是 .NET 可能存在内存泄漏。例如,GDI leaks 在 Windows 窗体应用程序中很常见。我帮助开发的其中一个应用程序经常遇到这种情况。当办公室的员工整天使用它的多个实例时,达到 Windows 固有的 10,000 个 GDI 对象限制的情况并不少见。

【讨论】:

GDI 对象是否包装在 .NET 对象中? 是的,GDI 对象是直接从 .NET 创建的。例如,Brush 类在幕后创建一个画笔 GDI 对象。垃圾收集器通常会清理这些东西,但显然我们发现了一些不会清理的情况。不管怎样,这是一个 .NET 1.1 应用程序,正如我们所说,它正在被 WPF 应用程序所取代。并且 WPF 不使用 GDI 对象。所以这是解决它的一种方法。 Brush 对象在其 Finalize 中调用 Dispose,所以我怀疑泄漏最终会被 GC 清除。尽管如此,即使它不是适当的泄漏,也绝对是在释放资源时出现不必要的延迟,这是一种 泄漏。【参考方案6】:

如果您不是指使用 .NET 的应用程序,这些答案对此进行了很好的讨论,但实际上指的是运行时本身,那么它在技术上可能存在内存泄漏,但在这一点上垃圾收集器的实现可能几乎没有错误。我听说过一种情况,在这种情况下,在运行时或标准库中的某些内容存在内存泄漏时发现了一个错误。但我不记得它是什么(非常模糊的东西),而且我想我再也找不到它了。

【讨论】:

+1 谢谢,这是我没有意识到的一个重要区别。【参考方案7】:

我想可以编写软件,例如.NET 运行时环境(CLR),如果足够小心,它不会泄漏内存。但由于 Microsoft 不时通过 Windows Update 发布 .NET 框架的更新,我相当确定即使在 CLR 中也存在偶尔的错误。

所有软件都可能泄漏内存。

但正如其他人已经指出的那样,还有其他类型的内存泄漏。虽然垃圾收集器处理“经典”内存泄漏,但仍然存在释放所谓的非托管资源(如数据库连接、打开的文件、GUI 元素等)的问题。 .这就是IDisposable 接口的用武之地。

另外,我最近在 .NET-COM 互操作 设置中遇到了可能的内存泄漏。 COM 组件使用引用计数来决定何时可以释放它们。 .NET 为此添加了另一种引用计数机制,该机制可以通过静态 System.Runtime.InteropServices.Marshal 类进行影响。

毕竟,即使在 .NET 程序中,您仍然需要小心资源管理。

【讨论】:

实现 IDisposable 的对象几乎总是实现终结器,因此底层资源最终会被 GC 清理,尽管不一定及时。 并非总是如此。有些root自己,在AppDomain关闭之前不会被清理。 @kyor:这很有趣。你能给我举一个你所说的那种自生根对象的例子吗?【参考方案8】:

这是 .NET 中的内存泄漏示例,它不涉及 unsafe/pinvoke,甚至不涉及事件处理程序。

假设您正在编写一个后台服务,该服务通过网络接收一系列消息并对其进行处理。所以你创建一个类来保存它们。

class Message 

  public Message(int id, string text)  MessageId = id; Text = text; 
  public int MessageId  get; private set; 
  public string Text  get; private set; 

好的,到目前为止一切顺利。后来您意识到,如果您在处理时参考了先前可用的消息,那么系统中的某些要求肯定会变得更容易。想要这个可能有很多原因。

所以你添加了一个新属性...

class Message

  ...
  public Message PreviousMessage  get; private set; 
  ...

然后你编写代码来设置它。当然,在主循环的某个地方,你必须有一个变量来跟上最后一条消息:

  Message lastMessageReceived;

然后你发现比你的服务更晚了几天,因为它已经用一长串过时的消息填满了所有可用的内存。

【讨论】:

这与其说是泄漏,不如说是糟糕的设计。它所做的正是所要求的:将所有以前的消息保存在内存中。它无法知道可以丢弃哪些消息。 @Steven Sudit - 计算机什么时候不完全按照它的要求做?内存泄漏是由糟糕的设计或错误的实现引起的。您可以将这种假设情况称为错误或糟糕的设计,但无论哪种方式,其目的都不是用无用的对象填充内存。 诚然,我很挑剔。只有当它不完全明显会无限期地增加内存使用时,我才会认为它是泄漏。您显示的代码与严重泄漏具有相同的症状 - 内存使用量增长直到崩溃 - 但它有不同的原因。然而,忘记取消链接事件处理程序是很微妙的。 让我澄清一下我所说的。在 C 中,如果你 malloc 一个缓冲区并丢失了对它的所有引用,那就是泄漏,因为此时你的代码无法恢复丢失的内存。在上面的示例中,代码可以“泄漏”对链的顶部引用,GC 将释放所有它。清理的可能性使我无法接受真正的泄漏。 这是一个泄漏,但不是传统泄漏。 .Net 排除了传统泄漏,但仍有许多方法可以创建非传统泄漏。【参考方案9】:

我发现的最好的例子实际上来自 Java,但同样的原则也适用于 C#。

我们正在读取由许多长行组成的文本文件(每行在堆中只有几 MB)。从每个文件中,我们搜索了几个关键子字符串并只保留了这些子字符串。在处理了几百个文本文件后,内存不足。

结果表明 string.substring(...) 会保留对原始长字符串的引用...即使我们只保留了 1000 个左右的字符,这些子字符串仍然会使用几个 MB 的内存。实际上,我们将每个文件的内容保存在内存中。

这是导致内存泄漏的悬空引用示例。 substring 方法试图重用对象,但最终浪费了内存。

编辑:不确定这个特定问题是否困扰 .NET。这个想法是为了说明在垃圾收集语言中执行的实际设计/优化,在大多数情况下,这种语言是智能且有用的,但可能会导致不必要的内存使用。

【讨论】:

我不相信这个特定的问题会困扰 .NET String.Substring 方法。 @Steven Studit - .Net 垃圾收集器使用称为 LargeObjectHeap 的东西,它的收集方式与其他代非常不同。 .Net 不喜欢扔掉“大物体”(只有大约 80K,iirc)。这与他在这里描述的不完全一样,但它解释了他的症状。 @Steve, Joel:不确定子字符串问题是否会影响 .NET,但它描述了垃圾收集系统中由未实现的优化引起的“内存泄漏”。 对,我理解您的帖子,因为引用缓存是一种明显的内存泄漏形式。我在 .NET 下的 SMO 类的过度缓存中看到了这一点。【参考方案10】:

您绝对可以在 .NET 代码中出现内存泄漏。在某些情况下,某些对象会根植于自己(尽管这些对象通常是IDisposable)。在这种情况下,未能在对象上调用 Dispose() 绝对会导致真正的 C/C++ 风格的内存泄漏,并且分配的对象是您无法引用的。

在某些情况下,某些计时器类可能具有这种行为,例如。

如果您有可能重新安排自身的异步操作,则可能存在泄漏。异步操作通常会根回调对象,防止收集。在执行过程中,对象被执行线程根,然后新调度的操作重新根对象。

这里有一些使用System.Threading.Timer的示例代码。

public class Test

    static public int Main(string[] args)
    
        MakeFoo();
        GC.Collect();
        GC.Collect();
        GC.Collect();
        System.Console.ReadKey();
        return 0;
    

    private static void MakeFoo()
    
        Leaker l = new Leaker();
    


internal class Leaker

    private Timer t;
    public Leaker()
    
        t = new Timer(callback);
        t.Change(1000, 0);
    

    private void callback(object state)
    
        System.Console.WriteLine("Still alive!");
        t.Change(1000, 0);
    

很像GlaDOS,Leaker 对象将无限期地“仍然存在” - 然而,没有办法访问该对象(除了内部,对象如何知道它何时不再被引用?)

【讨论】:

尽管如此,此示例中仍有运行代码。回调方法将每隔一段时间运行一次。在与之相关的任何代码也“死”之前,这不是传统的内存泄漏。您也许可以使用类似的方法来创建传统的内存泄漏,但所示示例尚不符合条件。【参考方案11】:

这里已经有一些很好的答案,但我想补充一点。让我们再仔细看看你的具体问题:


据我了解,框架本身是用 C++ 编写的,而 C++ 容易出现内存泄漏。

底层框架写得这么好,绝对没有内存泄露的可能吗? 框架的代码中是否有一些东西可以自我管理甚至修复它自己的潜在内存泄漏? 我还没有考虑其他答案吗?

这里的关键是区分你的代码和他们的代码。 .Net 框架(以及 Java、Go、python 和其他垃圾收集语言)承诺,如果您依赖他们的代码,您的代码不会泄漏内存。 . 至少在传统意义上。您可能会发现自己处于某些对象未按预期释放的情况,但这些情况与传统的内存泄漏略有不同,因为这些对象在您的程序中仍然可以访问。

您感到困惑,因为您正确理解这与说您创建的任何程序根本不可能有传统的内存泄漏是不同的。 他们的代码中仍然可能存在泄漏内存的错误。

所以现在您必须问自己:您更愿意相信您的代码还是他们的代码?请记住,他们的代码不仅经过原始开发人员的测试(就像您的一样,对吗?),而且还经过成千上万(也许数百万)像您这样的其他程序员的日常使用,经过了实战考验。任何重大的内存泄漏问题都将首先被识别和纠正。同样,我并不是说这是不可能的。只是相信他们的代码通常比你自己的代码更好……至少在这方面。

因此,这里的正确答案是它是您第一个建议的变体:

底层框架写得这么好,绝对没有内存泄露的可能吗?

不是没有可能,而是比自己管理要安全得多。我当然不知道框架中有任何已知漏洞。

【讨论】:

Eric Lippert 应该阅读整个问题(希望在这里获得虚荣搜索)。 补充一点:他们的代码确实存在内存泄漏(WPF 有很多泄漏,例如:connect.microsoft.com/VisualStudio/feedback/details/529736/…)。不试图抨击(我喜欢.net :)),只是举个例子。 @Michael:嗯,有他们的代码,然后是他们的代码。那就是有像 WPF ProgressBar 这样的东西,还有像 String 类型这样的东西。 WPF ProgressBar 与 我们的 代码一样有可能包含一两个奇怪的错误,但是我们都依赖的基本 .NET 代码(themus ) 出现此类错误的可能性要小得多。 谢谢。你通过区分我的代码和他们的代码来消除我的困惑。如果,当我运行我的托管代码时,有内存的证据,并且我发现泄漏发生在框架的错误部分,那么我的代码仍然不被认为是泄漏。泄漏的是框架。如果我的代码要在框架的非故障版本上运行,就不会发生泄漏。因此,即使存在内存泄漏,我的托管代码仍然不包含内存泄漏。这是我之前无法调和的矛盾。【参考方案12】:

以下是此人使用 ANTS .NET Profiler 发现的其他内存泄漏:http://www.simple-talk.com/dotnet/.net-tools/tracing-memory-leaks-in-.net-applications-with-ants-profiler/

【讨论】:

有趣的链接。 ANTS 帮助我确认 SMO 正在泄漏,所以我同样推荐它。 +1 在我上一份工作中,我们使用此分析器来查找我们正在使用的第 3 方 dll 中的内存泄漏。在那种情况下,它恰好是 dll 的不安全代码而不是托管代码中的泄漏,但它在帮助我们找到我们怀疑的泄漏方面仍然非常有用。【参考方案13】:

如果您使用托管 dll 但 dll 包含不安全的代码怎么办?我知道这很麻烦,但是如果您没有源代码,那么从您的角度来看,您只是在使用托管代码,但仍然可以泄漏。

【讨论】:

【参考方案14】:

.Net 中不存在有效的 C/C++ 内存泄漏的一个主要来源是何时释放共享内存

以下内容来自 Brad Abrams 领导的关于设计 .NET 类库的课程

“嗯,第一点当然是没有内存泄漏,对吧?不是吗?还有内存泄漏?嗯,还有一种不同的内存泄漏。那怎么样?那么内存的那种我们没有的泄漏是,在旧世界中,您过去常常 malloc 一些内存,然后忘记执行 free 或添加 ref 并忘记执行释放,或者任何一对。而在新世界中,垃圾收集器最终拥有所有内存,当不再有任何引用时,垃圾收集器将释放这些东西。但是仍然可能存在泄漏,对吗?什么是泄漏?好吧,如果您保留对那个对象是活着的,那么垃圾收集器就不能释放它。很多时候,发生的事情是你认为你已经摆脱了整个对象图,但仍然有一个人用引用抓住它,并且那么你就被卡住了。除非你删除所有对它的引用,否则垃圾收集器无法释放它。

我认为另一个问题是个大问题。没有内存所有权问题。如果你去阅读 WIN32 API 文档,你会看到,好吧,我先分配这个结构并把它传入,然后你填充它,然后我释放它。还是我告诉你大小,你分配它,然后我稍后释放它,或者你知道,关于谁拥有该内存以及应该在哪里释放它的所有这些争论都在进行。很多时候,开发人员只是放弃了,然后说,“好吧,随便。好吧,当应用程序关闭时它会免费,”这不是一个好计划。

在我们的世界中,垃圾收集器拥有所有托管内存,因此不存在内存所有权问题,无论您创建它并将其传递给应用程序,应用程序创建并开始使用它。这些都没有问题,因为没有歧义。垃圾收集器拥有这一切。 "

Full Transcript

【讨论】:

【参考方案15】:

请记住,缓存和内存泄漏之间的区别在于策略。如果您的缓存有一个错误的策略(或更糟糕的是,没有)来删除对象,那么它与内存泄漏是无法区分的。

【讨论】:

这让我想起了 Raymond Chen 的一篇文章的标题“A cache with a bad policy is another name for a memory leak” 康拉德:的确,这就是我在这里发帖的灵感。【参考方案16】:

此参考说明如何在 .Net 中使用弱事件模式发生泄漏。 http://msdn.microsoft.com/en-us/library/aa970850.aspx

【讨论】:

以上是关于为啥.NET 没有内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Netty ByteBuf.readBytes 会导致内存泄漏?

为啥这段代码不会导致内存泄漏? [复制]

为啥这会导致内存泄漏?

追踪 .NET Windows 服务内存泄漏

为啥 Rust 认为泄漏内存是安全的?

为啥苹果提供的 SimpleFTPSample 会泄漏内存?