Как избежать переполнения стека и утечек памяти в C#
Что проверяют
Понимаете ли вы, как в .NET устроены стек и управляемая куча, и знаете ли, что утечка при сборщике мусора выглядит иначе, чем в C++. Следом обычно просят назвать типичные источники утечек и рассказать, как вы искали бы утечку в работающем сервисе.
Как отвечать
Про стек. Переполнение почти всегда даёт бесконечная или слишком глубокая рекурсия: обход глубокого дерева, парсер вложенных структур, свойство, которое по ошибке обращается само к себе (get => Name вместо поля). Важно: StackOverflowException в .NET не перехватывается, процесс просто завершается. Что делать:
- итерация вместо рекурсии: обход через явный
Stack<T>илиQueue<T>, глубину ограничивает только куча; - предел глубины: рекурсивный разбор внешних данных останавливать своим исключением, а не молча обрезать результат;
- осторожность со
stackalloc: большие буферы брать изArrayPool<T>.
Хвостовую рекурсию C# не гарантирует, рассчитывать на неё нельзя.
Про память. Сборщик убирает недостижимые объекты, значит утечка это объект, на который кто-то всё ещё держит ссылку:
- события: подписчик не отписался через
-=, а издатель живёт дольше, например статический; - статические кеши без лимита: вместо самописного словаря брать
MemoryCacheс размером и сроком жизни; - замыкания и таймеры: лямбда держит весь объект,
Timerдержит колбэк; - неосвобождённые
IDisposable:usingосвобождает неуправляемые ресурсы, хэндлы и соединения, а не управляемую память, но их утечка тоже роняет сервис.
Искать так: dotnet-counters показывает рост кучи, dotnet-gcdump снимает снимок, а dotMemory или PerfView показывают, кто держит объекты.
Где спотыкаются
Говорят «в C# утечек нет, есть GC». Интервьюер попросит пример с событием, и ответ развалится.
Предлагают ловить StackOverflowException в catch. Это не работает, процесс завершается раньше.
Советуют WeakReference как универсальное средство. Это узкий инструмент для кешей и слабых событий, а утечку из-за забытой отписки лечат отпиской.