.NET Internals. Stack and Heap

.NET Internals. Stack and Heap

Дмитрий Бахтенков

Стэк и куча

Содержание:

  • Разделение памяти

Стек

  • Отслеживание стека вызовов
  • Хранение типов значений

Куча

  • Хранение ссылочных типов
  • Ссылочные vs значимые типы

Разделение памяти

По-умолчанию, когда .net-приложение запущено и для процесса выделено виртуальное адресное пространство, в куче будут следующие структуры данных:

  • Code Heap - хранит JIT-скомпилированный нативный код
  • Small Object Heap (SOH) - хранит объекты размером меньше 85 кб
  • Large Object Heap (LOH) - хранит объекты размером больше 85кб (исключение - массивы double размером больше 1000, которые тоже хранятся там)
  • Process Heap

Причиной разделения хранения мелких и крупных объектов на разные кучи является производительность. Об этом мы поговорим в следующих постах о сборке мусора.

Объекты, располагаемые в куче, имеют свои адреса, которые являются своего рода указателями на то место в памяти, где размещён определённый объект.

Куча, однако, не единственная структура данных, используемая процессом .net. Там также есть стек, который помогает отслеживать выполнение кода и хранит специальные типы данных. Теперь мы подробно рассмотрим как организовываются и используются стек и куча.

Стэк

Стэк - это структура данных, организованная по принципу LIFO - последний вошёл, первый вышел. Если подумать, он идеально подойдёт для всего, что скоро понадобится, так как это легко можно будет достать с верхушки стека. Такая природа стека приводит его к двум основным назначениям: отслеживание порядка выполнения и хранение переменных значимых типов.

Отслеживание порядка выполнения - стек вызовов

Большая часть кода, который мы пишем, упакована в классы и методы, которые могут вызывать другие методы, которые вызывают другие методы… Платформа .net framework должна всё время отслеживать порядок этого выполнения. Кроме того, она также должна запоминать состояние переменных и параметров метода, вызывающего другой метод (чтобы иметь возможность восстановить состояние предыдущего вызова метода).

Всё это поведение проиллюстрировано ниже:

Call stack frames, source: Wikipedia

Стэк используется для хранения порядка вызовов и часто упоминается как стек вызовов (Call stack), стек выполнения ( Execution stack) или стек программы ( Program stack)

Взгляните на следующий код:

void Method1()
  {
      Method2(12);
      Console.WriteLine("Goodbye");
  }
  void Method2(int testData)
  {
      int multiplier = 2;
      Console.WriteLine("Value is " + testData);
      Method3(testData* multiplier);
  }
  void Method3(int data)
  {
      Console.WriteLine("Double " + data);
  }
  // source: C. Farrell and N. Harrison – Under the Hood of .NET Memory Management

Чтобы вызвать Method2, .net должен сохранить адрес возврата выполнения, который будет следующей строкой кода для выполнения после завершения Method2. Этот адрес вместе с локальными переменными и параметрами вызываемого и вызывающего метода хранится в стеке вызовов, как показано ниже:

Call stack for methods 1-3, source: C. Farrell and N. Harrison – Under the Hood of .NET Memory Management

Вы также можете заметить, что происходит, когда возвращается Метод3 (его кадр стека выскакивает из стека – он исчезает).

Хранение значимых типов

Стэк также используется для хранения любых значимых типов в .net, таких как bool, decimal, int и тд. Полный список значимых типов .net можно найти тут (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types)

Значимые типы - это типы, которые хранят данные и память? в одном месте. Также интересно знать, что локальные переменные значимых типов, размещённые в стеке, очищаются после выполнения метода. Это происходит потому, что кадр стека становится метода недоступным - в стеке есть некоторый указатель на начало кадра стека верхнего уровня (указатель на текущий кадр стека), который просто перемещается на второй кадр стека сверху, как только выполнение метода завершено (данные всё ещё физически там, но недоступны с помощью механизмов основных .net)

Куча

Куча похожа на стек, но стек может быть представлен как несколько ящиков, сложенных друг на друга, где мы всегда можем положить ещё один ящик наверх или убрать первый сверху ящик. Куча содержит ящики, которые можно получить в любое время. Ящики в куче расположены в различных местах, не обязательно друг на друге. Для получения одного из них нам не нужно снимать сверху другие, нам нужно лишь знать место, где этот ящик лежит.

Хранение ссылочных типов

Все переменные, тип которых не является значимым, такие как string или object (а также классы, интерфейсы, делегаты и тд), называют ссылочными. Все ссылочные типы размещены в управляемой куче (большой или малой, зависит от размера объекта). Однако, несмотря на то, что объект размещён в куче, ссылка на него (адрес объекта в куче) хранится в стеке.

Рассмотрим следующий код:

 void Method1()
  {
      MyClass myObj = new MyClass();
      Console.WriteLine(myObj.ToString());
  }

На рисунке ниже показано, как здесь будет выглядеть стек и куча с точки зрения выделенных данных:

Stack and Heap with reference variable, source: C. Farrell and N. Harrison – Under the Hood of .NET Memory Management

"OBJREF" хранится в стеке и является указателем (ссылкой) на объект MyClass в куче.

Выражение 'MyClass myObj' не размещает в куче объект myObj. Оно только создаст "OBJREF" в стеке, инициализировав его значением NULL. К моменту использования оператора new происходит фактическое выделение памяти в куче и устанавливается значение ссылки.

Значимые vs ссылочные типы (стек vs куча)

Ключевым отличием между ссылочными и значимыми типами заключается в том, что когда переменная значимого типа передаётся в другой метод как параметр или просто присваивается другой переменной, это значение будет скопировано в новую переменную. Вот почему когда мы передаём переменную в другой метод, который её изменяет, исходное значение не изменяется. Это поведение отражено на картинке:

Value types copying, source: link

В случае ссылочных типов всё по-другому. Когда одной ссылочной переменной мы присваиваем другую переменную ссылочного типа, её значение не будет скопировано, вместо этого будет присвоена ссылка на тот же объект (адрес в памяти, где размещены данные). По сути, новая переменная (или параметр метода) по-прежнему указывает на то же место в памяти, поэтому изменение такой переменной ссылочного типа внутри вызываемого метода будет действовать за пределами метода. Этот случай проиллюстрирован ниже.

References passing, source: link

Конечно, хранение некоторых данных в стеке и некоторых в куче имеет свою определённую цель, к которой мы перейдём в дальнейшем.


Report Page