【问题标题】:Is it possible to have a memory leak in managed code? (specifically C# 3.0)托管代码中是否可能存在内存泄漏? (特别是 C# 3.0)
【发布时间】:2011-09-20 03:59:30
【问题描述】:

例如,如果我有一个分层数据结构:

class Node
{
    public List<Node> children;
}

它被填充到许多层次,然后在其中一个父母中去:

myNode.children.Clear();

这将清除所有对直系子女的引用 - 但是那些直系子女引用的所有孙辈、孙辈等呢? C# 是否足够聪明,知道不再需要它们并且它们将被垃圾回收?

我读过使用 WPF 数据绑定而不实现接口 INotifyChanged 会导致内存泄漏:http://blogs.msdn.com/b/micmcd/archive/2008/03/07/avoiding-a-wpf-memory-leak-with-databinding-black-magic.aspx,这在托管环境中怎么可能?

【问题讨论】:

标签: c# .net memory-leaks


【解决方案1】:

当然,由于 C# 尤其缺少大家都在谈论的其他引用分配,如果您有一个包装原生资源但从未处理过的类(或者您丢失了对它的引用),您可能会造成泄漏。

这是 Image 类的一个示例:

public static void MemLeak()
{
    var src = @"C:\users\devshorts\desktop\bigImage.jpg";

    Image image1 = null;

    foreach (var i in Enumerable.Range(0, 10))
    {
        image1 = Image.FromFile(src);
    }

    image1.Dispose();

    Console.ReadLine();
}

Image 是一次性的,所以由于我在最后处理了图像,所以应该不会有泄漏吧?实际上,您每次都用新图像覆盖引用的事实意味着您无法处置旧图像引用持有的底层 GDI+ 资源。这将引入内存泄漏。

由于 gc doesn't call dispose for you 和 Image 类没有覆盖 Finalize 方法(并在那里调用 Dispose),那么您就有了泄漏。

【讨论】:

    【解决方案2】:

    是的,C# 中的泄漏是由于在不再需要这些对象后未正确删除对对象的引用而导致的。如果对对象的引用已被删除,则垃圾收集器在运行时会删除该对象(它有时会根据仔细调整的算法确定的时间自动执行此操作,因此最好不要手动使其运行,除非你真的知道你在做什么!)。但是如果没有正确删除对对象的引用,垃圾收集器仍然认为应用程序需要它,因此内存泄漏。在没有正确删除的事件处理程序中发现这种情况尤其常见。如果具有子/孙对象的所有引用都已删除,则该对象以及所有这些子/孙对象也将在下次运行垃圾收集器时被删除(除非它们也被其他地方引用)。

    最好的办法是使用内存分析器,它可以让您查看哪些对象在内存中保存了其他对象(大多数让您拍摄内存快照,然后查看显示引用的某种图表。如果一个对象仍然当它不应该存在时,您可以查看一个图表,显示哪个引用将该对象保存在内存中,并使用它来确定您应该清除引用以避免内存泄漏的位置。有一些分析器可用,但我通过red gate找到最容易使用的ant memory profilerhttp://www.red-gate.com/products/dotnet-development/ants-memory-profiler/。

    【讨论】:

      【解决方案3】:

      内存泄漏基本上是程序正常行为不再需要的一块内存,但由于编程错误而无法释放。所以内存泄漏的概念与垃圾回收、C# 或 Java 无关。

      举个例子:

      var list = new List<Node>();
      Node a1 = new Node();
      Node a2 = new Node();
      // ...
      Node an = new Node();
      
      // Populate list
      list.Add(a1);
      list.Add(a2);
      // ...
      list.Add(an);
      
      // use this list
      DoStuffTo(list);
      
      // clear list -- release all elements
      list.Clear();
      
      // memory leaks from now on
      

      注意列表中的元素是如何发生内存泄漏的,因为它们被变量a1 ... an引用

      这只是一个简单的例子,说明为什么不只是由 C# 来处理内存泄漏。解决此问题也是开发人员的责任:

      // Clear references
      a1 = null;
      a2 = null;
      // ...
      an = null;
      

      这将告诉 C# 垃圾收集器应该收集所有这些元素。

      【讨论】:

      • 发布模式下的编译器足够聪明,可以知道您不再使用这些值,因此实际上会清除它们。您无需将它们显式设置为 null。
      • +1 有趣——所以我们可以假设这些是字段而不是局部变量。或者我们可能会在 lambdas a.s.o. 中使用这些局部变量
      【解决方案4】:

      顺便说一句,如果您使用 unsafe 关键字,您也可能会在 .net 中出现内存泄漏。如果您以与 c++ 等相同的方式使用指针,并且不小心确保您不会“丢失”指针引用,那么 GC 将无法收集它。

      不安全块示例;

      unsafe
      {
      int * ptr1, ptr2;
      ptr1 = &var1;
      ptr2 = ptr1;
      *ptr2 = 20;
      }
      

      【讨论】:

        【解决方案5】:

        C# 不在乎。执行 GC 是 CLR 的工作。

        GC 从已知的根对象(静态字段、局部变量……)开始,遍历引用,直到找到所有可访问的对象。可以收集所有其他对象(不包括一些与终结器相关的东西)。

        因此,如果子引用确实是对这些对象的唯一引用,那么大子引用也会被收集。但是,如果某个活动的外部对象仍然具有对您的节点之一的引用,则该节点和它所引用的所有其他对象都将保持活动状态。


        托管内存泄漏是由使对象保持活动状态的引用引起的。

        例如,当使用数据绑定时,GUI 会引用使它们保持活动状态的对象。

        类似地,订阅事件会使与事件处理程序关联的对象保持活动状态。所以有时事件会使用弱引用来避免这个问题。

        【讨论】:

          【解决方案6】:

          我建议阅读 .net 世界中如何处理垃圾收集 - 本质上,它通过遵循引用来查找任何可以被顶级对象引用并释放的内容其他一切;它不适用于像 C++ 世界这样的析构函数,因此如果托管对象的父对象和祖父对象被释放,您会很高兴知道托管对象将“消失”。

          当然,垃圾收集器只知道托管内存,如果您有任何非托管资源,则值得查看IDisposable pattern - 这允许确定性释放非托管对象。

          在处理可能引用对象时会出现复杂的情况,它确实包括一些不太明显的东西,例如事件处理程序,这就是您提到的 WPF/INotifyPropertyChanged 问题的来源。

          【讨论】:

            【解决方案7】:

            .NET 中可能存在某种内存泄漏。

            如果您有一个对象“A”注册到另一个对象“B”上的事件,那么“B”将获得对“A”的引用,并且如果您在“A “超出范围。在这种情况下,“A”不能被垃圾收集,因为仍然有一个活动的引用。它会一直存在,直到“B”被垃圾回收。

            如果您遇到“A”对象被创建并不断超出范围的情况,您将在内存中获得越来越多的“A”。

            【讨论】:

              【解决方案8】:

              循环引用对于 .NET 中的 GC 没有问题。它使用一种算法来确定哪些对象实际上可以从某些入口点(例如 main 方法)到达。

              然而,可能导致内存泄漏的是静态成员意外引用的对象。

              您的示例属于第一类,因此可以安全使用。

              【讨论】:

                【解决方案9】:

                垃圾收集器只收集不再使用的对象 - 内存泄漏是由对象仍然持有对对象的引用引起的,即使它们不应该。

                在您的情况下,如果另一个对象使用了一个大孩子,那么 .Clear 会将其从节点列表中删除,但垃圾收集器不会收集它。但它会收集所有其他的孙子。

                例子:

                class Foo {
                 public Node SomeProperty {get; set;}
                
                    public void SomeFunction(){
                        var node = new Node { children = new List<Node>() };
                        var childNode = new Node();
                        var childNode2 = new Node();
                        node.children.Add(childNode);
                        node.children.Add(childNode2);
                        SomeProperty = childNode2;
                
                        node.children.Clear();
                        // childNode will be garbage collected
                        // childNode2 is still used by SomeProperty,
                        // so it won't be garbage collected until SomeProperty or the instance
                        // of Foo is no longer used.
                    }
                }
                

                【讨论】:

                  【解决方案10】:

                  是的,您可以拥有一个完整的对象图(海量数据结构),但如果没有一个对象被绑定或引用,它将被垃圾回收。

                  如果不是,则表明 GC 尚未运行(您可以尝试使用 GC.Collect() 进行诊断,但不应在生产代码中使用它)或者某些内容引用了结构的一部分。例如,UI 可能会绑定到它。

                  【讨论】:

                    【解决方案11】:

                    是的,垃圾收集器会发现孙子等是垃圾。基本上,如果无法获取某个对象,则该对象被视为垃圾并符合收集条件。

                    至于在托管代码中内存“泄漏”是如何可能的——通常是如果您最终得到一个对象,该对象可以通过对象引用访问,但在没有办法最终“清除”的情况下" 通过 API 引用这些引用。

                    你引用的博文就是这样:

                    存在 WPF 检查以查找实现 INotifyProperyChanged 的​​内容的问题。如果有一个数据绑定到没有实现这个接口的东西,那么它会在全局表中创建一条记录。该记录不会被清理,因为 WPF 无法检查何时不再需要该数据库记录。

                    所以有这个全局表维护引用,你没有办法表明表中的项目可以被清除。

                    【讨论】:

                    • 我认为我们应该使用术语对象泄漏来表示对象存在引用但无法清理的情况。使用术语“内存泄漏”意味着 GC 以某种方式被破坏,从而导致误解和类似的问题。
                    • @Stilgar:也许吧。我并不完全相信——我怀疑在任何一种情况下你都必须解释你的意思。
                    • 可能,但如果我们解释一下,让我们有所作为。内存泄漏在概念上与对象泄漏不同。由于内存泄漏,您的程序拥有一些内存,但没有人知道这意味着什么。这种记忆实际上丢失了。这是在 C 中发生的事情,在 C# 中不会发生。对象泄漏会使用内存,但众所周知,该内存代表某个对象,它甚至可以做一些工作,比如响应它订阅的事件。它不再只是记忆,而是更高层次的抽象。
                    • 不用担心围绕“泄漏”的 Stilgar 撇号暗示它不是传统意义上的泄漏。
                    猜你喜欢
                    • 1970-01-01
                    • 2018-11-26
                    • 2012-09-27
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-02-13
                    相关资源
                    最近更新 更多