Как возникают утечки памяти в C# и примеры их возникновения
Что проверяют
Понимаете ли вы, как работает сборщик мусора в .NET, и отличаете ли настоящую утечку от объекта, который просто ещё нужен. Следом почти всегда спрашивают, как вы найдёте утечку в работающем сервисе.
Как отвечать
Сборщик мусора в .NET трассирующий: он обходит граф от корней (статические поля, стеки потоков, GCHandle) и освобождает всё недостижимое. Утечка в управляемом коде это объект, до которого случайно осталась ссылка. Типичные источники:
- события:
publisher.Changed += OnChangedкладёт в делегат ссылку на подписчика. Если издатель живёт дольше, например синглтон или статическое событие, подписчик не соберётся, пока не сделает-=; - статические коллекции и кэши:
static Dictionaryбез вытеснения растёт вечно. Для кэша берутMemoryCacheсSizeLimitи сроками жизни записей; - замыкания: лямбда, сохранённая в долгоживущем объекте, держит всё, что захватила, включая
this; - DI:
IDisposable-сервис с временем жизниTransient, полученный из корневого провайдера, контейнер держит до своего уничтожения. Похожий эффект даёт scoped-зависимость, захваченная синглтоном; - неуправляемые ресурсы: забытый
Disposeу потоков, соединений,Bitmap. Финализатор когда-нибудь освободит дескриптор, но непредсказуемо, а память изMarshal.AllocHGlobalбезFreeHGlobalне вернётся никогда.
Как искать: dotnet-counters показывает, растёт ли куча после сборок, dotnet-gcdump или dotMemory снимают два снимка с разницей во времени, а команда gcroot в dotnet-dump выводит цепочку ссылок, которая держит объект.
Где спотыкаются
Называют циклические ссылки причиной утечки. Это верно для подсчёта ссылок, а в .NET два объекта, ссылающиеся друг на друга и недостижимые из корней, собираются спокойно.
Лечат рост памяти вызовом GC.Collect(). Достижимый объект принудительная сборка не тронет, а паузы станут чаще.
Думают, что Dispose освобождает память объекта. Он освобождает ресурсы, а саму память заберёт сборщик, когда объект станет недостижим.