Утечки памяти на основе событий С#

У меня есть приложение, которое имеет некоторые утечки памяти из-за того, что события не отсоединяются до того, как ссылка на объект установлена ​​в значение 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 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

События сложны, потому что, когда A подписывается на B, оба заканчивают связь друг с другом. В вашем примере это не проблема, поскольку утечки нет (созданный B и является единственным объектом для хранения ссылки на B, поэтому оба A и B умрут, когда A умрет).

Для реальных проблем с событиями, что бы решить проблему, это понятие "слабые события". К сожалению, единственный способ получить слабые события на 100% - это поддержка CLR. Microsoft, похоже, не заинтересована в предоставлении этой поддержки.

Я рекомендую вам "слабые события Google в С#" и начинаю читать. Вы найдете много разных подходов к решению проблемы, но вы должны знать об их ограничениях. Нет 100% -ного решения.

Ответ 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);
           }

    }

Ответ 4

Расширение идеи IDisposable, которую предложил @Max Малыгин:

В приведенном ниже коде показано, как проверить выдающиеся обработчики на событии.

Класс имеет событие Tick, которое срабатывает один раз в секунду. Когда вызывается Dispose, код перечисляет обработчики в списке вызовов (если они есть) и выводит имя класса и метода, все еще подписанное на событие.

Программа создает экземпляр объекта, прикрепляет обработчик события, который записывает "галочку" при каждом запуске события, а затем спит в течение 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

  • Dump Target Obj, чтобы получить таблицу методов: ! dumpobj 01e19248
  • Таблица методов дампа, чтобы найти в нем 0040C060: ! dumpmt -md 0ced1910
  • Если нет совпадения, Dump память, которая начинается с адреса _methodPtr: ! u 0040C060
  • Найдите инструкцию JMP или MOVE и дамп их адрес, например: ! u 0cf54930

Посетите здесь для более подробной информации: http://radheyv.blogspot.com/2011/04/detecting-memory-leaks-in-silverlight.html