Ответ
Утечка памяти в управляемом коде 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.
}
Другие частые причины утечек:
- Статические коллекции:
public static class Cache { public static List<HeavyObject> GlobalCache = new List<HeavyObject>(); } // Объекты, добавленные в GlobalCache, никогда не будут собраны GC. - Таймеры и колбэки: Объект, зарегистрированный как колбэк в
System.Threading.Timer, будет удерживаться в памяти. - Неуправляемые ресурсы: Классы, реализующие
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.
}
Вот смотри, что тут происходит: ты создаёшь подписчика, он цепляется за ивент издателя, а потом просто выбрасываешь ссылку на него. А издатель-то, сука, этого не знает! У него в списке делегатов висит твой метод, а значит и ссылка на объект, которому этот метод принадлежит. И всё, приехали — объект будет висеть вечно, пока жив издатель. Красота, да? Пиздец.
Ещё причины, по которым память уплывает в никуда:
-
Статические коллекции — могильник твоих объектов.
public static class Cache { public static List<HeavyObject> GlobalCache = new List<HeavyObject>(); } // Объекты, добавленные в GlobalCache, никогда не будут собраны GC.Ну тут всё просто, как три копейки: положил в статик — и прощай. GC на статику не покушается, она же корень. Будут лежать там до конца времён, как египетские мумии.
-
Таймеры и колбэки. Зарегистрировал свой объект как колбэк в
System.Threading.Timer— и всё, он теперь заложник. Таймер будет держать его, пока сам не помрёт. -
Неуправляемые ресурсы. Ну это вообще отдельная песня. Класс реализовал
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, у него есть политики вытеснения старых данных. Пусть он сам думает, что удалять.
Короче, суть в чём: в управляемом коде память сама не потечёт, только если ты её не организуешь. Держи ссылки в порядке, следи за статикой и событиями, используй правильные инструменты — и будет тебе счастье. А нет — получишь медленный, захлебнувшийся от памяти сервис и ночные дежурства с профайлерами. Выбор за тобой, ёпта.