JavaScript, стек и куча

Олег Кусов06.11.2021
JavaScript

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

  • Стек
  • Куча

Стек

Стек нужен для хранения значений переменных запущенной функции. Состоит он из фреймов, где каждому фрейму соотносится запущенная функция.

Стек в ОЗУ соотносится со структурой данных "стек", работает по принципу LIFO (Last In, First Out), а его размер равен 1 мегабайту. Когда запускается функция, она записывается в стек и даже без наличия переменных внутри функции, будет занимать определенное место в стеке. Очень часто при бесконечной рекурсии на JS можно встретить в консоли сообщение Maximum call stack size exceeded error. Оно говорит о том, что наш стек при последовательных вызовах функции попросту заполнился.

Куча

В отличие от стека куча не имеет ограничений по размеру и может увеличиваться при необходимости. В куче хранятся все не примитивные значения (массивы, хэш-таблицы, классы и так далее).

В JS нам не нужно как-то управлять состоянием кучи, за нас это делает сборщик мусора. Однако в C++ его нет, поэтому программистам приходится самим управлять памятью своего приложения, это позволяет лучше поработать над оптимизацией, но при этом увеличивает нагрузку на разработчика и перекладывает на него все риски в виде утечек памяти.

Но давайте всё же поймем как работает куча. Для этого предлагаю рассмотреть пример:

После запуска в стеке будет создан фрейм функции, в нём будут параметры функции, локальные переменные, возвращаемое значение. Можно заметить, что в функции объявляется массив. Массив - это объект, поэтому он будет создан в куче, а в переменной bar в стеке будет храниться лишь ссылка на адрес массива в памяти. Когда функция выполнится - стек будет автоматически очищен. И сборщик мусора при отсутствии ссылок на наш массив удалит его из кучи.

Чтобы записать наш массив в кучу , JavaScript должен вызвать функцию операционной системы sbrk(N), где аргумент N - количество выделяемой памяти. Эта функция выделяет память и возвращает адрес выделенной памяти. Под капотом разные языки программирования могут по разному управлять кучей. Например, в C++ используется связный список, когда каждый выделенный участок памяти последовательно ссылается друг на друга. Это позволяет лучше управлять состоянием кучи, и избавляет от ошибок, когда на один и тот же участок памяти могут ссылаться несколько переменных.

Освобождение кучи происходит с помощью той же системной функции sbrk(-N). При передаче ей отрицательного значения, размер кучи будет уменьшен на это значение. В случае со связным списком при освобождении пространства не в конце списка, а где-то посередине, в C++ объявляется структура для каждого сегмента выделенной памяти с переменной is_free, которая становится равной единице.

И в случае последующей записи данных в кучу, если в нашем связном списке будут свободные области (is_free = 1), данные будут записаны в них без необходимости выделения дополнительной памяти для кучи.

Что произойдет, когда мы попытается сделать так:

В этом случае в стеке появится фрейм функции sampleFunc, внутри фрейма будут наши локальные переменные bar, baz, foo и bam. Что же они будут хранить?

  • bar хранит ссылку на адрес массива в куче
  • baz хранит двоичное представление строки 'Hello',
  • foo хранит то же, что и bar
  • bam хранит то же, что и baz. То есть bam не ссылается на baz, а полностью хранит у себя копию нашего примитива.

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

Источники: arjunsreedharan.org glebbahmutov.com