【问题标题】:C# Event Based Memory Leaks基于 C# 事件的内存泄漏
【发布时间】:2010-11-20 17:55:48
【问题描述】:

我有一个应用程序,由于在对象引用设置为 null 之前未分离事件而导致一些内存泄漏。应用程序很大,通过查看代码很难找到内存泄漏。我想使用 sos.dll 来查找作为泄漏源的方法的名称,但我被卡住了。我设置了一个测试项目来演示这个问题。

这里我有 2 个类,一个有一个事件,并在监听该事件,如下所示

namespace MemoryLeak
{
    class Program
    {
        static void Main(string[] args)
        {
            TestMemoryLeak testMemoryLeak = new TestMemoryLeak();

            while (!Console.ReadKey().Key.Equals('q'))
            {
            }
        }
    }

    class TestMemoryLeak
    {
        public event EventHandler AnEvent;

        internal TestMemoryLeak()
        {
            AnEventListener leak = new AnEventListener();
            this.AnEvent += (s, e) => leak.OnLeak();
            AnEvent(this, EventArgs.Empty);
        }

    }

    class AnEventListener
    {
        public void OnLeak()
        {
            Console.WriteLine("Leak Event");
        }
    }
}

我闯入代码,并在中间窗口输入

.load sos.dll

然后我使用 !dumpheap 来获取 AnEventListener 类型的堆上的对象

!dumpheap -type MemoryLeak.AnEventListener

我得到以下信息

PDB symbol for mscorwks.dll not loaded
 Address       MT     Size
01e19254 0040348c       12     
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
0040348c        1           12 MemoryLeak.AnEventListener
Total 1 objects

我使用 !gcroot 来找出对象没有被垃圾回收的原因

!gcroot 01e19254

并得到以下

!gcroot 01e19254
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Error during command: Warning. Extension is using a callback which Visual Studio 
does not implement.

Scan Thread 5208 OSTHread 1458
ESP:2ef3cc:Root:01e19230(MemoryLeak.TestMemoryLeak)->
01e19260(System.EventHandler)->
01e19248(MemoryLeak.TestMemoryLeak+<>c__DisplayClass1)->
01e19254(MemoryLeak.AnEventListener)
Scan Thread 7376 OSTHread 1cd0

我现在可以看到作为泄漏源的事件处理程序。我用 !do 来查看 事件处理程序的字段并获取

!do 01e19260
Name: System.EventHandler
MethodTable: 65129dc0
EEClass: 64ec39d0
Size: 32(0x20) bytes
   (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
65130770  40000ff        4        System.Object  0 instance 01e19248 _target
6512ffc8  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
6513341c  4000101        c        System.IntPtr  1 instance 0040C060 _methodPtr
6513341c  4000102       10        System.IntPtr  1 instance 00000000 _methodPtrAux
65130770  400010c       14        System.Object  0 instance 00000000 _invocationList
6513341c  400010d       18        System.IntPtr  1 instance 00000000 _invocationCount

所以现在我可以看到指向没有被分离的方法的指针

0040C060 _methodPtr

但我如何获得该方法的名称?

【问题讨论】:

  • 请参阅我对这个问题的回答,了解如何获取 _methodPtr 指向的 stackoverflow.com/questions/3668642/…
  • 哇,这是对调试检查的非常透彻的解释。赞成花时间发布一个详细的例子——它将来可能会被其他人使用。我会收藏这个。

标签: c# memory-leaks


【解决方案1】:

事件很棘手,因为当 A 订阅 B 时,两者最终都持有对彼此的引用。在您的示例中,这不是问题,因为没有泄漏(A 创建了 B 并且是唯一持有对 B 的引用的对象,因此当 A 死亡时 A 和 B 都会死亡)。

对于真实事件问题,解决它的是“弱事件”的概念。不幸的是,获得 100% 工作的弱事件的唯一方法是在 CLR 的支持下。 Microsoft 似乎没有兴趣提供这种支持。

我建议您在 Google 上搜索“C# 中的弱事件”并开始阅读。你会发现许多不同的方法来解决这个问题,但你必须意识到它们的局限性。没有 100% 的解决方案。

【讨论】:

  • 嘿 Tergiver,感谢您提供的信息。我将查看弱事件以供将来参考。我正在处理的应用程序的问题是,有一个协调类在应用程序的整个生命周期中都存在。正是这个类上的事件导致了内存泄漏,因为任何订阅者都通过引用这个协调类上的事件来保持活动状态。
  • 在处理没有弱事件模式的静态事件时,您唯一的选择是在最后一个引用消失之前取消订阅(在此之后您无法取消订阅)。
  • 我应该说,“最后一个可见参考”。
【解决方案2】:

如何实现旧的 IDisposable 呢?

        class TestMemoryLeak : IDisposable
        {
              public event EventHandler AnEvent;
              private bool disposed = false;

           internal TestMemoryLeak()
           {
                 AnEventListener leak = new AnEventListener();
                 this.AnEvent += (s, e) => leak.OnLeak();
                AnEvent(this, EventArgs.Empty);
           }

           protected virtual void Dispose(bool disposing)
           {
              if (!disposed)
               {
                 if (disposing)
                 {
                      this.AnEvent -= (s, e) => leak.OnLeak();
                 }
                 this.disposed = true;
                }

            }

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

    }

【讨论】:

  • 嗨,Max,感谢您的回复。在这个例子中会很酷,但在实际应用程序中,有很多代码,我不知道需要分离的方法的名称,所以我试图找出来......
  • @Gaz:但是您可以使用它来枚举附加到事件的代表,并检查每个代表的Method 属性,以找出谁没有分离。
【解决方案3】:

我建议使用 .NET 内存分析器来解决问题的核心,那里有几个 - 我个人过去曾使用过 Red Gate ANTS memory profiler,它确实有 14 天的免费试用期:

http://www.red-gate.com/products/ants_memory_profiler/walkthrough.htm

【讨论】:

    【解决方案4】:

    扩展@Max Malygin 提出的IDisposable 想法:

    下面的代码显示了如何检查事件中未完成的处理程序。

    该类有一个每秒触发一次的Tick 事件。当调用Dispose 时,代码会枚举调用列表中的处理程序(如果有),并输出仍订阅事件的类和方法名。

    程序实例化一个对象,附加一个事件处理程序,每次触发事件时都会写入“tick”,然后休眠 5 秒。然后它在不取消订阅事件处理程序的情况下处理对象。

    using System;
    using System.Diagnostics;
    using System.Threading;
    
    namespace testo
    {
        public class MyEventThing : IDisposable
        {
            public event EventHandler Tick;
            private Timer t;
    
            public MyEventThing()
            {
                t = new Timer((s) => { OnTick(new EventArgs()); }, null, 1000, 1000);
            }
    
            protected void OnTick(EventArgs e)
            {
                if (Tick != null)
                {
                    Tick(this, e);
                }
            }
    
            ~MyEventThing()
            {
                Dispose(false);
            }
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
    
            private bool disposed = false;
            private void Dispose(bool disposing)
            {
                if (!disposed)
                {
                    if (disposing)
                    {
                        t.Dispose();
                        // Check to see if there are any outstanding event handlers
                        CheckHandlers();
                    }
    
                    disposed = true;
                }
            }
    
            private void CheckHandlers()
            {
                if (Tick != null)
                {
                    Console.WriteLine("Handlers still subscribed:");
                    foreach (var handler in Tick.GetInvocationList())
                    {
                        Console.WriteLine("{0}.{1}", handler.Method.DeclaringType, handler.Method.Name);
                    }
                }
            }
    
        }
    
        class Program
        {
            static public long Time(Action proc)
            {
                Stopwatch sw = Stopwatch.StartNew();
                proc();
                return sw.ElapsedMilliseconds;
            }
    
            static int Main(string [] args)
            {
                DoIt();
                Console.WriteLine();
                Console.Write("Press Enter:");
                Console.ReadLine();
                return 0;
            }
    
            static void DoIt()
            {
                MyEventThing thing = new MyEventThing();
                thing.Tick += new EventHandler(thing_Tick);
                Thread.Sleep(5000);
                thing.Dispose();
            }
    
            static void thing_Tick(object sender, EventArgs e)
            {
                Console.WriteLine("tick");
            }
        }
    }
    

    输出是:

    Handlers still subscribed:
    testo.Program.thing_Tick
    

    【讨论】:

      【解决方案5】:

      你可以在 WinDbg 上这样尝试

      1. 转储目标对象以获取方法表:!dumpobj 01e19248
      2. 转储方法表以在其中找到 0040C060:!dumpmt -md 0ced1910
      3. 如果没有匹配,则转储从_methodPtr地址开始的内存:!u 0040C060
      4. 找到 JMP 或 MOVE 指令并转储它们的地址,例如:!u 0cf54930

      更多详情请访问:http://radheyv.blogspot.com/2011/04/detecting-memory-leaks-in-silverlight.html

      【讨论】:

        猜你喜欢
        • 2010-12-24
        • 1970-01-01
        • 2016-01-27
        • 2010-11-11
        • 2017-02-18
        • 1970-01-01
        • 2013-11-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多