Стек против Кучи

Во время работы программы данные должны помещаться в память. Объём памяти, занимаемый данными, и место их хранения определяются типом данных. При выполнении программы для хранения информации используются два участка памяти: Стек (Stack) и Куча (Heap).

Стек (Stack)

Стек — это непрерывный массив памяти, построенный по принципу последним пришёл — первым ушёл (LIFO). В нём хранятся три категории данных:

  • Значения переменных отдельных типов
  • Текущий контекст выполнения программы
  • Параметры, передаваемые в методы
Стек похож на тубу для монет: добавлять и извлекать элементы можно только с одного конца.

Операции с памятью стека управляются системой автоматически, разработчику не нужно выполнять ручные манипуляции. Понимание низкоуровневой логики стека упрощает анализ хода выполнения кода и изучение официальной документации C#.

Основные свойства стека

  • Добавление и удаление данных возможно только на вершине стека
  • Запись данных на вершину стека: операция Push (заталкивание)
  • Извлечение данных с вершины стека: операция Pop (выталкивание)

Особенности памяти стека

  • Очень высокая скорость выделения и освобождения памяти, используется непрерывный участок ОЗУ
  • Время жизни привязано к вызову метода: после завершения работы метода фрейм стека уничтожается автоматически
  • Хранится: значения переменных типов значений, параметры методов, локальные переменные, контекст выполнения

Чтобы лучше разобраться с принципом работы кучи, рассмотрим простой пример кода

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Code language: JavaScript (javascript)

Логика этого примера простая. С первой строки точки входа Main создаётся переменная целого числа a, на второй строке объявляется переменная b. Третья строка вызывает функцию Add, передавая значения a и b внутрь метода. Переменная x получает значение a, y — значение b, функция возвращает сумму x + y равную 30, результат передаётся обратно в точку вызова и записывается в sum. После завершения работы Add переменные x и y выталкиваются из стека, переменная sum принимает значение 30, после завершения Main программа закрывается. Далее мы разберём по шагам весь процесс заталкивания и выталкивания элементов стека.

Ход выполнения программы и последовательность операций Push/Pop для стека

При создании переменной a встроенные базовые типы выделяются напрямую в стеке с операцией Push
Выполняется вторая строка кода, создаётся вторая целочисленная переменная b со значением 20. Ранее добавленные элементы лежат на дне стека, новые — на его вершине
В третьей строке сначала выполняется правая часть выражения Add(a,b), происходит вызов функции. Значения a и b копируются в x и y, это передача по значению, поэтому a и x — совершенно разные независимые переменные.
На данном шаге выполняется только вычисление x + y, результат не сохраняется в стеке надолго. Число 30 возвращается в точку вызова, как если бы написано int sum = 30;. После выполнения return x+y функция Add готовится к завершению, все локальные переменные внутри метода выталкиваются из стека, выделенная им память очищается для повторного использования.
Самая верхняя позиция стека называется вершиной (Top), после операции Pop указатель вершины смещается вниз.
После завершения Add x и y — локальные параметры этой функции, они последовательно выталкиваются, освобождая место в стеке для стабильной работы системы.
После выталкивания x и y исчезают, возвращённое значение 30 поступает в точку вызова для записи в sum.
При выполнении строки int sum = 30 повторяется операция Push: переменная sum со значением 30 заталкивается на вершину стека. После этой инструкции Main готовится к завершению, локальные переменные sum, b, a будут последовательно удалены операцией Pop.
Операция Pop всегда выполняется сверху вниз. Стек похож на тупик с единственным входом и выходом, все элементы добавляются и извлекаются с одного конца. Поэтому порядок удаления переменных в Main обратный порядку их создания: сначала sum, потом b, затем a.
Переменная b выталкивается и уничтожается, указатель вершины стека автоматически смещается вниз.
Все переменные вытолкнуты из стека, последняя переменная a также удалена. Строго говоря, у Main есть ещё одна локальная переменная — массив строк args из Main(string[] args), он принимает аргументы командной строки при запуске консоли. Данные этого массива хранятся в двух разных участках памяти, этот момент связан с кучей, которую мы разберём позже, поэтому в данном примере мы не рассматриваем args.

Запомните важное правило: при выполнении функции переменные создаются сверху вниз и заталкиваются в стек последовательно a → b → вход в Add → x → y. После завершения Add x и y выталкиваются, затем заталкивается sum, при окончании Main переменные удаляются в обратном порядке: sum, b, a.


Куча (Heap)

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

Программа не может вручную удалять объекты из кучи, очистку не ссылающихся недоступных объектов выполняет автоматически сборщик мусора GC среды CLR (Общеязыковая среда выполнения). Это избавляет разработчиков от ошибочных ручных операций освобождения памяти, характерных для других языков.

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

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

Алгоритм работы сборщика мусора GC

  1. Программа создаёт три объекта в куче и хранит ссылки на них
  2. Программа перестаёт использовать один из объектов, все ссылки на него пропадают
  3. GC обнаруживает не ссылаемый объект и освобождает занимаемую им память
  4. Сбор завершён, освобождённый участок памяти выделяется под новые объекты

Особенности памяти кучи

  • Память не непрерывная, скорость выделения ниже, чем у стека
  • Время жизни не привязано к границам метода, управляется централизованно сборщиком GC
  • Хранится: экземпляры ссылочных типов (классы, массивы, делегаты и т.д.); в стеке сохраняются только адреса памяти объектов из кучи

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

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    //Поля класса
    private string name;

    private int age;

    //Конструктор, разберём позже
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    //Метод класса
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Code language: JavaScript (javascript)

На схеме ниже показано расположение в памяти четырёх переменных Main: a, b, tom, lucy. Как и при разборе стека, код выполняется сверху вниз: сначала заталкивается a, затем b, значения этих переменных хранятся напрямую в стеке.

Переменные базовых типов хранятся напрямую в стеке
Данные пользовательского класса Student хранятся в куче, в стеке записывается только адрес участка памяти кучи с объектом.
Например, данные tom лежат в блоке кучи 00001, в стеке сохраняется адрес 00001 для последующего поиска данных.

После завершения работы Main ссылка lucy: 01010 выталкивается из стека и уничтожается, но удаляется только адрес, а не сами данные объекта в куче. В этот момент объект кучи теряет все ссылки из переменных стека.

Данные объекта Lucy в блоке кучи 01010 больше не имеют ссылок из стека, становятся не востребованными. Сборщик мусора GC уничтожит их в подходящий момент.
Объект Lucy в куче теряет все внешние ссылки после выталкивания адреса из стека. GC очистит память этого объекта в произвольный момент для повторного использования участка. Если на объект остаётся хотя бы одна ссылка, GC не уничтожит его.

Участки памяти

Стек и куча — логические разделы памяти на уровне программного обеспечения, не физическое разделение оборудования.

Стек и куча являются лишь программным логическим разбиением памяти, физическая ОЗУ не имеет отдельных аппаратных блоков с пометкой «зона стека» или «зона кучи».

Физическая оперативная память RAM состоит из единых микросхем с последовательными физическими адресами, никаких маркеров разделения стека и кучи в аппаратуре нет

Операционная система выделяет каждому запущенному процессу отдельное виртуальное адресное пространство, которое предварительно делит на четыре основные логические сегмента:

Сегмент кода: хранит скомпилированные инструкции программы

Глобальный статический сегмент: статические переменные, константы

Сегмент стека: основной и дочерние потоки программы имеют каждый собственный независимый участок стека

Сегмент кучи: большой общий пул памяти для динамического выделения

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

Стек против Кучи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *