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

Приведи пример, где происходит утечка памяти в C#

1.0 Junior🔥 62 комментариев
#Основы C# и .NET

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

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

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

Утечка памяти в C#: классический пример с обработчиками событий

В C#, несмотря на наличие сборщика мусора (Garbage Collector), утечки памяти возможны из-за неправильного управления ссылками. Один из самых распространенных сценариев — удержание объектов в памяти через обработчики событий (event handlers).

Проблема: циклические ссылки через события

Рассмотрим пример, где Publisher генерирует события, а Subscriber на них подписывается. Если подписчик не отписывается, возникает утечка:

// Издатель события
public class EventPublisher
{
    public event EventHandler? SomethingHappened;
    
    public void RaiseEvent()
    {
        SomethingHappened?.Invoke(this, EventArgs.Empty);
    }
}

// Подписчик
public class EventSubscriber
{
    private readonly string _id;
    
    public EventSubscriber(string id, EventPublisher publisher)
    {
        _id = id;
        // Подписываемся на событие
        publisher.SomethingHappened += OnSomethingHappened;
    }
    
    private void OnSomethingHappened(object? sender, EventArgs e)
    {
        Console.WriteLine($"Subscriber {_id} received event");
    }
}

Тестовый код, демонстрирующий утечку

public class MemoryLeakDemo
{
    public static void RunLeakExample()
    {
        var publisher = new EventPublisher();
        
        for (int i = 0; i <和无符号1_000_000; i++)
        {
            var subscriber = new EventSubscriber($"Subscriber_{i}", publisher);
            // Создали подписчика, но НЕ отписались от события
            // Объект subscriber должен быть удален, но остается в памяти
        }
        
        // Принудительный сбор мусора
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        
        // Несмотря на GC, publisher удерживает ссылки на все подписчики
        // через делегаты SomethingHappened
    }
}

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

  1. Механизм работы делегатов: Когда Subscriber подписывается на событие через publisher.SomethingHappened += OnSomethingHappened, издатель сохраняет ссылку на метод подписчика, а значит — на сам объект Subscriber.

  2. Циклическая ссылка:

    • EventPublisher → (через делегат) → EventSubscriber
    • EventSubscriber → (через поле) → EventPublisher
  3. Сборщик мусора не может очистить память: GC определяет объекты как "живые", если на них есть достижимые ссылки. Издатель удерживает подписчиков, даже если другие части кода уже на них не ссылаются.

Визуализация проблемы

[Publisher] 
    ↓ (хранит в списке инвокеров события)
[Delegate 1] → [Subscriber 1] → ссылка назад на Publisher
[Delegate 2] → [Subscriber 2] → ссылка назад на Publisher
[Delegate 3] → [Subscriber 3] → ссылка назад на Publisher
... миллионы объектов!

Решение: правильная отписка от событий

public class FixedSubscriber : IDisposable
{
    private readonly string _id;
    private EventPublisher? _publisher;
    
    public FixedSubscriber(string id, EventPublisher publisher)
    {
        _id = id;
        _publisher = publisher;
        publisher.SomethingHappened += OnSomethingHappened;
    }
    
    private void OnSomethingHappened(object? sender, EventArgs e)
    {
        Console.WriteLine($"Subscriber {_id} received event");
    }
    
    public void Dispose()
    {
        // КРИТИЧЕСКИ ВАЖНО: отписываемся от события
        if (_publisher != null)
        {
            _publisher.SomethingHappened -= OnSomethingHappened;
            _publisher = null; // обрываем циклическую ссылку
        }
    }
}

Другие распространенные причины утечек в C#

  • Статические коллекции: Объекты, добавленные в static List<T> или словари, никогда не освобождаются.
  • Кэши без политики очистки: In-memory кэши, которые растут бесконтрольно.
  • Таймеры: System.Timers.Timer или System.Threading.Timer, если не остановлены.
  • Неосвобождаемые ресурсы: Невызванные Dispose() для Bitmap, Stream, DbContext и т.д.
  • Анонимные методы и замыкания: Подписка на события через лямбда-выражения, захватывающие внешние переменные.

Как диагностировать утечки?

  1. Используйте Memory Profiler в Visual Studio или JetBrains dotMemory
  2. Анализируйте дампы памяти с помощью PerfView или WinDbg
  3. Мониторьте счетчики производительности: Process\Private Bytes, .NET Memory\Gen 2 Collections

Ключевой вывод: В управляемых языках типа C# утечки памяти — это не "потерянные указатели", а непреднамеренное удержание объектов в памяти через живые ссылки. События — классический пример, требующий дисциплины в управлении жизненным циклом объектов.

Приведи пример, где происходит утечка памяти в C# | PrepBro