Приведи пример утечки памяти в C#.

«Приведи пример утечки памяти в C#.» — вопрос из категории Управление памятью, который задают на 28% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Утечка памяти в управляемом коде C# — это ситуация, когда объекты остаются в памяти, потому что на них существуют корневые ссылки, хотя логически они уже не нужны. GC не может их собрать.

Классический пример: неправильная работа с событиями

public class EventPublisher
{
    public event EventHandler SomethingHappened;
}

public class EventSubscriber
{
    private string _data;
    public EventSubscriber(EventPublisher publisher, string data)
    {
        _data = data;
        // ПОДПИСКА: publisher теперь содержит ссылку на этот экземпляр Subscriber
        publisher.SomethingHappened += OnSomethingHappened;
    }

    private void OnSomethingHappened(object sender, EventArgs e)
    {
        Console.WriteLine(_data);
    }
    // ОТПИСКИ НЕТ! -> УТЕЧКА
}

// Использование:
var publisher = new EventPublisher();
while (true)
{
    var subscriber = new EventSubscriber(publisher, "leaking object");
    // После выхода из области видимости ссылка 'subscriber' теряется,
    // но publisher.SomethingHappened всё ещё держит делегат, ссылающийся на метод объекта.
    // Объект subscriber будет жить, пока жив publisher.
}

Другие частые причины утечек:

  1. Статические коллекции:
    public static class Cache
    {
        public static List<HeavyObject> GlobalCache = new List<HeavyObject>();
    }
    // Объекты, добавленные в GlobalCache, никогда не будут собраны GC.
  2. Таймеры и колбэки: Объект, зарегистрированный как колбэк в System.Threading.Timer, будет удерживаться в памяти.
  3. Неуправляемые ресурсы: Классы, реализующие IDisposable (файлы, соединения с БД, графические ресурсы), но не освобождаемые явно.

Диагностика и решение:

  • Инструменты: Используйте Memory Profiler в Visual Studio, dotMemory от JetBrains или PerfView.
  • Решение для событий: Реализуйте IDisposable в подписчике и отписывайтесь.

    public class EventSubscriber : IDisposable
    {
        private EventPublisher _publisher;
        public EventSubscriber(EventPublisher publisher)
        {
            _publisher = publisher;
            _publisher.SomethingHappened += OnSomethingHappened;
        }
    
        public void Dispose()
        {
            _publisher.SomethingHappened -= OnSomethingHappened; // КРИТИЧНО
        }
    }
  • Для кэшей: Используйте WeakReference или готовые реализации (MemoryCache с политикой вытеснения).