Зачем нужна куча в C#
Что проверяют
Понимаете ли вы модель памяти .NET, а не заученную фразу «ссылочные типы в куче, значимые в стеке». Следом обычно спрашивают про поколения сборщика мусора и про то, как уменьшить число аллокаций.
Как отвечать
Стек хранит кадры вызовов и живёт строго по вложенности методов: метод вернулся, кадр исчез. Куча нужна для всего, что должно пережить вызов или чей размер известен только во время выполнения: объекты ссылочных типов, массивы, строки. Их можно вернуть из метода, положить в коллекцию, передать другому потоку. К тому же стек маленький, по умолчанию около мегабайта на поток.
Дальше про управляемую кучу:
- быстрое выделение: аллокация это сдвиг указателя в участке, закреплённом за потоком, почти как на стеке;
- поколения: 0, 1 и 2. Большинство объектов умирают молодыми, поэтому сборка нулевого поколения частая и дешёвая, а полная редкая и дорогая;
- сжатие: после сборки живые объекты сдвигаются, и в обычной куче фрагментации почти нет;
- LOH: объекты от 85 000 байт идут в кучу больших объектов. Она собирается вместе со вторым поколением и по умолчанию не сжимается, отсюда фрагментация и совет переиспользовать большие буферы через
ArrayPool<T>; - POH: с .NET 5 отдельная куча для закреплённых объектов, чтобы pinning не мешал сжатию.
Покажите, что значимые типы живут там, где объявлены. Поле-struct внутри класса лежит в куче, упакованный int тоже, как и локальные переменные, захваченные лямбдой или пережившие await в async-методе. Для горячих путей без аллокаций есть Span<T>, stackalloc, ValueTask.
Где спотыкаются
Повторяют правило «struct всегда на стеке». Первый же вопрос про упаковку или замыкание его опровергает.
Считают, что сборщик мусора освобождает файлы и соединения. Он освобождает только память, для остального нужны IDisposable и using, а финализатор лишь продлевает жизнь объекта ещё на одну сборку.
Советуют вызывать GC.Collect() для экономии памяти. Обычно это переносит живые объекты в старшие поколения и делает паузы только хуже.