Что происходит, если в куче не хватает памяти и сборщик мусора не может освободить место?
Что проверяют
Знаете ли вы, что происходит после неудачной сборки, и понимаете ли, что нехватка памяти в управляемом приложении редко означает буквальное «память кончилась». Заодно смотрят, умеете ли вы такое диагностировать, а не только называть исключение.
Как отвечать
Последовательность такая. Выделение не помещается в текущий сегмент, рантайм запускает полную блокирующую сборку старшего поколения, пробует уплотнить кучу и запросить память у системы. Не помогло, значит в точке выделения возникает OutOfMemoryException.
Дальше объясните, почему до этого доходит:
- утечка по достижимости: статические коллекции и кэши без вытеснения, подписки на события, которые никто не снимает, объекты, оставшиеся в закрытых окнах. Сборщику такие объекты кажутся живыми, и формально он прав;
- фрагментация кучи больших объектов. Массивы и строки от восьмидесяти пяти тысяч байт попадают в отдельную кучу, которая по умолчанию не уплотняется: свободного места много, а непрерывного куска под новый массив нет;
- закрепление объектов через
fixedилиGCHandleмешает уплотнению и обычной кучи; - внешний лимит: в контейнере процесс упирается не в адресное пространство, а в ограничение памяти.
Диагностика: счётчики по поколениям и по куче больших объектов, снимок кучи и поиск корней, которые держат подозрительные объекты.
Где спотыкаются
Обещают перехватить исключение и продолжить работу. Обработчик сам может не получить памяти, а состояние процесса уже неопределённое: часть операций оборвалась на середине. Такое исключение имеет смысл записать в журнал и завершиться, а не лечить на ходу.
Думают, что using и Dispose освобождают управляемую память. Они освобождают внешние ресурсы: дескрипторы, соединения, неуправляемые буферы. Место в куче возвращает только сборщик и только у недостижимых объектов.
В контейнере ждут исключения, а получают внезапную смерть процесса без единой записи в журнале: превышение лимита памяти обрабатывает ядро, и приложению никто ничего не сообщает. Понять это можно по коду завершения, а не по трассировке стека.