Приведи пример утечки памяти в 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 с политикой вытеснения).

Ответ 18+ 🔞

Да ты посмотри, какая интересная хуйня происходит в управляемом коде! Вроде всё за тебя должно убираться, ан нет — сидишь потом и чешешь репу, откуда гигабайты памяти уплывают.

Ну, в общем, утечка памяти в 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.

    Ну тут всё просто, как три копейки: положил в статик — и прощай. GC на статику не покушается, она же корень. Будут лежать там до конца времён, как египетские мумии.

  2. Таймеры и колбэки. Зарегистрировал свой объект как колбэк в System.Threading.Timer — и всё, он теперь заложник. Таймер будет держать его, пока сам не помрёт.

  3. Неуправляемые ресурсы. Ну это вообще отдельная песня. Класс реализовал IDisposable (файлы, соединения с БД), а ты его не диспознул. Ресурсы-то из-под CLR могут освободиться, а вот сам управляемый объект с навороченными полями может благополучно остаться в памяти, если на него где-то ссылка осталась. Хуёвая ситуация.

Чем ловить и как лечить?

  • Инструменты: Бери 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; // КРИТИЧНО
        }
    }

    Создал — используй, а потом в Dispose отцепился. Красота.

  • Для кэшей: Не городи свои велосипеды из List'ов. Используй WeakReference (слабые ссылки), но там тоже своих граблей дохуя. Или бери готовое, проверенное — MemoryCache, у него есть политики вытеснения старых данных. Пусть он сам думает, что удалять.

Короче, суть в чём: в управляемом коде память сама не потечёт, только если ты её не организуешь. Держи ссылки в порядке, следи за статикой и событиями, используй правильные инструменты — и будет тебе счастье. А нет — получишь медленный, захлебнувшийся от памяти сервис и ночные дежурства с профайлерами. Выбор за тобой, ёпта.