Memory Management: Найти утечку памяти
Условие
В следующем коде есть утечка памяти. Найдите и исправьте её.
public class EventManager { public event EventHandler<DataEventArgs> DataReceived; private static EventManager _instance = new EventManager(); public static EventManager Instance => _instance; }
public class DataProcessor { public DataProcessor() { EventManager.Instance.DataReceived += OnDataReceived; }
private void OnDataReceived(object sender, DataEventArgs e)
{
// Process data
}
}
// Использование: for (int i = 0; i < 10000; i++) { var processor = new DataProcessor(); // processor goes out of scope }
Задание:
- Объясните, почему происходит утечка памяти
- Предложите исправление (IDisposable, weak events)
- Как диагностировать такие утечки (dotMemory, PerfView)?
Критерии оценки:
- Понимание event subscription memory leak
- Знание паттернов решения
- Понимание GC roots
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Утечка памяти в Event Subscription
Почему происходит утечка памяти?
Проблема в паттерне event subscription:
- Статический EventManager — существует всю жизнь приложения
- Подписка на событие — когда
DataProcessorподписывается наDataReceived, EventManager сохраняет ссылку на экземплярDataProcessor - Отсутствие отписки — когда
processorвыходит из области видимости, он не удаляется из памяти, потому что EventManager всё ещё держит на него ссылку - GC корни — статический EventManager является GC корнем, через который все подписанные обработчики остаются в памяти
В цикле из 10000 итераций все 10000 экземпляров DataProcessor остаются в памяти одновременно, хотя они больше не используются.
Решение 1: IDisposable паттерн
public class DataProcessor : IDisposable
{
private bool _disposed = false;
public DataProcessor()
{
EventManager.Instance.DataReceived += OnDataReceived;
}
private void OnDataReceived(object sender, DataEventArgs e)
{
// Process data
}
public void Dispose()
{
if (!_disposed)
{
EventManager.Instance.DataReceived -= OnDataReceived;
_disposed = true;
}
}
}
// Использование:
for (int i = 0; i < 10000; i++)
{
using (var processor = new DataProcessor())
{
// processor использован
} // Dispose() вызывается здесь — отписка от события
}
Преимущества:
- Явная очистка ресурсов
- Полный контроль над жизненным циклом
- Работает во всех сценариях
Недостатки:
- Легко забыть вызвать Dispose()
- Требует заключать в
usingблоки
Решение 2: WeakEvent паттерн
public class EventManager
{
private List<WeakReference<DataProcessor>> _subscribers =
new List<WeakReference<DataProcessor>>();
public event EventHandler<DataEventArgs> DataReceived;
public void Subscribe(DataProcessor processor)
{
_subscribers.Add(new WeakReference<DataProcessor>(processor));
DataReceived += processor.OnDataReceived;
}
public void RaiseDataReceived(DataEventArgs args)
{
// Очистка мёртвых ссылок
_subscribers.RemoveAll(wr => !wr.IsAlive);
DataReceived?.Invoke(this, args);
}
}
Преимущества:
- Автоматическая очистка при GC
- Не требует явного Dispose()
- Более элегантно
Недостатки:
- Сложнее для понимания
- Небольшие накладные расходы на WeakReference
Решение 3: WeakEventManager (встроенный паттерн)
public class EventManager
{
public static event EventHandler<DataEventArgs> DataReceived;
public static void RaiseDataReceived(DataEventArgs args)
{
DataReceived?.Invoke(null, args);
}
}
public class DataProcessor
{
public DataProcessor()
{
WeakEventManager<EventManager, DataEventArgs>
.AddHandler(EventManager, nameof(EventManager.DataReceived), OnDataReceived);
}
private void OnDataReceived(object sender, DataEventArgs e)
{
// Process data
}
}
Диагностика утечек памяти
1. dotMemory (JetBrains):
- Запустить приложение с dotMemory
- Сделать снимок памяти до цикла
- Выполнить цикл с 10000 итерациями
- Сделать второй снимок
- Сравнить: увидите 10000 объектов DataProcessor
- Проанализировать GC корни (GC Roots explorer)
2. PerfView (Microsoft):
- Запустить PerfView
- Включить сбор ETW событий
- Выполнить приложение
- Проанализировать heap дамп
- Найти объекты, удерживаемые EventManager
3. Встроенный профайлер Visual Studio:
- Debug → Performance Profiler
- Выбрать Memory Usage
- Запустить сценарий
- Посмотреть Heap Size graph
- Увидеть неконтролируемый рост при проблеме
Рекомендация для этого кейса
Для синглтон EventManager — используйте IDisposable паттерн (Решение 1), так как это:
- Явно показывает намерение
- Легко отследить в коде
- Стандартная практика для resource cleanup
- Работает надёжно в production
Альтернатива для более сложных сценариев — WeakEventManager для полностью автоматического управления.
Key takeaways
- Event subscriptions = strong references — всегда помните об этом
- Синглтоны + события = опасное сочетание — требует особой внимательности
- Всегда отписывайте или используйте WeakEvent
- Профилируйте память — не гадайте о утечках