Как устроена память в C#?
Что проверяют
Понимаете ли вы разницу между значимыми и ссылочными типами и что делает сборщик мусора. Вопрос почти всегда ведёт к производительности: боксингу, лишним аллокациям и утечкам, которые в управляемой памяти всё равно случаются.
Как отвечать
Начните с двух областей:
- стек: кадр на каждый вызов метода, живёт до выхода из него. Дёшево, освобождение автоматическое, размер ограничен;
- куча: объекты со временем жизни дольше вызова, ими занимается сборщик мусора.
Дальше уточните частое заблуждение: «значимые типы всегда на стеке» неверно. Структура внутри класса лежит в куче вместе с ним, а замыкание или async-метод поднимает захваченные локальные переменные в объект на куче. Правильнее говорить, что тип определяет семантику присваивания: значимый копируется целиком, ссылочный копирует ссылку.
Про сборщик мусора расскажите поколениями: новые объекты попадают в поколение 0, пережившие сборку переходят в 1 и 2. Сборка нулевого поколения дешёвая и частая, второго дорогая и редкая, потому что затрагивает всю кучу. Отдельно живёт куча больших объектов (LOH, от 85 килобайт), она не уплотняется по умолчанию и фрагментируется.
Обязательно скажите про недетерминированность: GC освобождает память, но не закрывает файлы и соединения. Для этого есть IDisposable и using, а финализатор это запасной вариант, который откладывает освобождение ещё на один цикл сборки.
Если хотите усилить ответ, назовите способы снизить нагрузку: struct для мелких короткоживущих данных, Span<T> и stackalloc для работы без аллокаций, пулы (ArrayPool), отказ от боксинга через дженерики.
Где спотыкаются
Считают, что в C# утечек не бывает. Живая ссылка держит объект вечно: подписка на событие без отписки, статическая коллекция, забытый таймер.
Путают Dispose и сборку мусора, ожидая, что GC закроет соединение с базой.
Не знают про боксинг: значимый тип, положенный в object или в нетипизированную коллекцию, копируется в кучу, и в горячем цикле это заметно.