Как выстраивается иерархия объектов в памяти?
Что проверяют
Понимаете ли вы, как объект CLR устроен физически и как объекты связаны друг с другом для сборщика мусора. Главная ловушка вопроса: миф «значимые типы живут на стеке». Следом обычно спрашивают про поколения GC и упаковку.
Как отвечать
Начните с одного объекта ссылочного типа в управляемой куче:
- заголовок объекта: служебное слово для
lock, хэш-кода и индекса sync block; - указатель на MethodTable: через него объект знает свой тип, таблицу виртуальных методов и родительский тип;
- поля: при наследовании поля базового класса лежат первыми, за ними поля наследника, и всё это один непрерывный блок, а не цепочка объектов. Приведение к базовому типу ничего не создаёт, меняется только взгляд на ту же память.
На 64-битной платформе служебная часть занимает 16 байт, минимальный объект 24 байта.
Затем значимые типы. struct хранится там, где объявлен: локальная переменная на стеке или в регистре, поле класса внутри объекта в куче, элемент массива прямо в массиве. При приведении к object или интерфейсу происходит упаковка, и в куче появляется отдельный объект.
Дальше граф ссылок. Сборщик идёт от корней: статические поля, локальные переменные активных методов, GCHandle. Всё, до чего он не дошёл, считается мусором. Куча делится на поколения 0, 1 и 2, объекты от 85 000 байт попадают в Large Object Heap, а с .NET 5 закреплённые массивы можно выделять в Pinned Object Heap.
Где спотыкаются
Говорят, что struct всегда на стеке. Поле int в классе лежит в куче вместе с объектом, а локальная переменная, захваченная лямбдой, переезжает в объект замыкания.
Думают, что obj = null освобождает память. Объект лишь становится кандидатом на сборку, а когда придёт GC, решает рантайм. К тому же JIT в релизной сборке и без этого считает объект недостижимым после последнего использования.
Путают наследование с вложенностью и считают, что внутри наследника где-то лежит отдельный объект базового класса.