Как происходит выделение памяти для нового объекта в .NET
Что проверяют
Знаете ли вы, что происходит между new и первой строкой конструктора, и понимаете ли, почему аллокация в .NET дешёвая, а дорогой оказывается сборка мусора. Следом обычно спрашивают про поколения и Large Object Heap.
Как отвечать
Для ссылочного типа порядок такой:
- выделение:
newкомпилируется в IL-инструкциюnewobj, память берётся в куче малых объектов (SOH), в нулевом поколении. У каждого потока есть свой allocation context, заранее выделенный участок, и объект размещается простым сдвигом указателя. Это быстрееmalloc: нет поиска свободного блока и блокировки между потоками; - заголовок: перед полями лежат заголовок объекта (sync block index) и указатель на
MethodTable, через который работают приведение типов и виртуальные вызовы. Поэтому на 64-битной платформе даже пустой объект занимает 24 байта; - обнуление: память приходит заполненной нулями, поля имеют значения по умолчанию ещё до конструктора;
- конструктор: вызывается последним, причём инициализаторы полей в C# отрабатывают раньше конструктора базового класса.
Когда allocation context кончился, выделение идёт по медленному пути, и здесь может запуститься сборка нулевого поколения. Объекты от 85 000 байт, обычно большие массивы, сразу попадают в Large Object Heap: он собирается вместе со вторым поколением и по умолчанию не сжимается, отсюда фрагментация. Для закреплённых буферов с .NET 5 есть отдельный Pinned Object Heap.
Отдельно скажите про значимые типы. Они хранятся там, где объявлены: локальная переменная на стеке или в регистре, поле класса внутри объекта в куче. Упаковка (object o = 42) создаёт полноценный объект в куче.
Где спотыкаются
Говорят «структуры всегда на стеке, классы всегда в куче». Структура внутри класса или массива лежит в куче, а JIT в свежих версиях .NET умеет размещать на стеке некоторые объекты, которые не покидают метод.
Считают new дорогой операцией. Дорого не выделение, а давление на GC: частые короткоживущие аллокации в горячем цикле ведут к частым сборкам.
Не знают порога LOH и удивляются фрагментации при работе с большими буферами, хотя для них есть ArrayPool<T>.