Stack vs. Heap (.NET)
Stack
Известен также как стек потока (thread stack) или стек вызовов (call stack).
Небольшая область оперативной памяти (несколько Мб, точный размер зависит от платформы).
Древнейший механизм. Известен со времён Тьюринга.
Работает по принципу стопки (LIFO). Допустимые операции — добавить наверх (push) и снять сверху (pop). Не нужен поиск или какой-либо “уход” за структурой. Поэтому стек очень быстрый.
Через стек организуется вызов методов, передача аргументов, хранение локальных переменных и любых других данных с предопределённым временем жизни.
Авторитетные люди предлагают считать стек деталью реализации.
Но на практике важно понимать, что́ и в каких случаях остаётся на стеке, а что уходит в кучу. Это влияет на производительность.
Evaluation stack на уровне IL — это другое. “Настоящий” стек появляется вместе с процессом/потоком на уровне операционной системы.
У каждого потока свой изолированный стек.
CLR умеет ходить по стеку (stack walk, stack crawl), определять границы методов (кадры, фреймы), аргументы и переменные; отделять управляемый код от неуправляемого; формировать stack trace; делать unwind при ловле исключений.
Если на Windows подключиться к .NET-процессу отладчиком с галочкой Native, то можно увидеть путь от ядра через Program.Main
:
Name | Language |
---|---|
Project.dll !Program.Main(...) Line 29 |
C# |
[Native to Managed Transition] | |
hostpolicy.dll !coreclr::execute_assembly(...) |
|
. . . | |
dotnet.exe !__scrt_common_main_seh() Line 288 |
|
kernel32.dll !BaseThreadInitThunk() |
Unknown |
ntdll.dll !RtlUserThreadStart() |
Unknown |
А вот стек потока, запущенного через Thread.Start
:
Name | Language |
---|---|
Project.dll !Program.MyThreadProc() Line 47 |
C# |
... Thread.ThreadMain_ThreadStart() Line 93 |
C# |
... ExecutionContext.RunInternal(...) Line 167 |
C# |
[Native to Managed Transition] | |
kernel32.dll !BaseThreadInitThunk() |
Unknown |
ntdll.dll !RtlUserThreadStart() |
Unknown |
Оба примера наглядно демонстрируют, что стек начинается вместе с user-mode потоком в недрах операционной системы, .NET добавляет свои фреймы сверху, а отладчик видит на всю глубину (при наличии отладочных символов).
Heap
Известна также как управляемая куча (managed heap).
Здесь динамически выделяется и освобождается память под всё, что не получилось разместить на стеке или в регистрах процессора.
Выделение памяти в куче описывается словом аллокация.
Аллокации бывают явными (оператор
new
) и неявными (boxing и[CompilerGenerated]
объекты, возникающие при обессахаривании замыканий, итераторов,async
-методов).Освобождение неиспользуемой памяти автоматическое — в CLR встроен сборщик мусора (garbage collector, GC).
На самом деле, GC занимается всеми аспектами автоматического управления памятью — запрашивает у операционной системы “сырые” регионы (через
VirtualAlloc
илиmmap
), выделяет место под объекты, делит объекты на поколения, собирает статистику, принимает решения, когда и в каких поколениях искать недостижимые объекты, утилизирует и дефрагментирует неиспользуемое пространство.“Garbage collection is simulating a computer with an infinite amount of memory" — Raymond Chen
Konrad Kokosa подробно рассказывает про внутренности .NET GC. А ещё он смог прикрутить свой собственный коллектор (очень интересно!)
Слово “управляемая” добавляют именно потому, что .NET-куча всецело находится под управлением GC.
Куч может быть много. Есть Large Object Heap и Pinned Object Heap. В некоторых режимах GC заводит по куче на каждое ядро процессора. Но поскольку объекты в разных кучах могут ссылаться друг на друга, можно называть кучей всю их совокупность.
Вместе с каждым объектом хранятся
ObjHeader
иMethodTable
pointer.ObjHeader
используется для кеширования hash code, синхронизации черезMonitor
и разных флагов.MethodTable
pointer — это указатель на метаданные. Благодаря ему каждый объект в куче самодостаточен, знает о своём конкретном типе, что в свою очередь делает возможным динамическое приведение типов, вызов виртуальных методов и Runtime Reflection.