Утечки памяти на основе событий С#
У меня есть приложение, которое имеет некоторые утечки памяти из-за того, что события не отсоединяются до того, как ссылка на объект установлена в значение 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);
}
}
Ответ 3
Я бы рекомендовал использовать профилировщик .NET памяти, чтобы понять суть этого вопроса, есть несколько - лично я использовал Red Gate ANTS профайлер памяти в прошлом, у которого есть 14-дневная бесплатная пробная версия:
http://www.red-gate.com/products/ants_memory_profiler/walkthrough.htm
Ответ 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