C#基于事件的内存泄漏

16

我有一个应用程序存在内存泄漏问题,这是由于在将对象引用设置为null之前未解除事件绑定。该应用程序相当大,通过查看代码很难找到内存泄漏的位置。我想使用sos.dll来查找导致泄漏的方法名称,但我卡住了。我设置了一个测试项目来展示这个问题。

这里有两个类,其中一个有一个事件,另一个监听该事件,代码如下:

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

但是我该如何获取那个方法的名称?


1
请查看我的回答,了解如何获取_methodPtr指向的内容。https://dev59.com/mHA65IYBdhLWcg3wuRF8#3682594 - Brian Rasmussen
2
哇,这是一个非常详尽的调试检查解释。点赞你花时间发布了一个详细的例子 - 它可能会在将来为其他人带来用处。我会收藏这个页面的。 - Tom W
5个回答

4
事件很棘手,因为当 A 订阅 B 时,两者最终都会持有对方的引用。在您的示例中,这不是问题,因为没有泄漏(A 创建了 B 并且是唯一一个持有对 B 的引用的对象,因此当 A 死亡时,A 和 B 都将死亡)。
对于真正的事件问题,解决它的是“弱事件”的概念。不幸的是,要获得100%工作的弱事件,唯一的方法是 CLR 的支持。微软似乎没有提供此支持的兴趣。
我建议您搜索“C#中的弱事件”并开始阅读。您将找到许多不同的解决问题的方法,但必须注意它们的限制。没有100%的解决方案。

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

1

1

那么实现一下好老的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
@Gaz:但是你可以使用这个来枚举附加到事件的委托,并检查每个委托的Method属性,以找出谁没有解除绑定。 - Jim Mischel

1

在 @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

0

你可以在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


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