← Назад к вопросам

Memory Management: Найти утечку памяти

2.0 Middle🔥 201 комментариев
#Память и Garbage Collector

Условие

В следующем коде есть утечка памяти. Найдите и исправьте её.

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 }

Задание:

  1. Объясните, почему происходит утечка памяти
  2. Предложите исправление (IDisposable, weak events)
  3. Как диагностировать такие утечки (dotMemory, PerfView)?

Критерии оценки:

  • Понимание event subscription memory leak
  • Знание паттернов решения
  • Понимание GC roots

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Утечка памяти в Event Subscription

Почему происходит утечка памяти?

Проблема в паттерне event subscription:

  1. Статический EventManager — существует всю жизнь приложения
  2. Подписка на событие — когда DataProcessor подписывается на DataReceived, EventManager сохраняет ссылку на экземпляр DataProcessor
  3. Отсутствие отписки — когда processor выходит из области видимости, он не удаляется из памяти, потому что EventManager всё ещё держит на него ссылку
  4. 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
  • Профилируйте память — не гадайте о утечках