Управление памятью

Введение

Низкоуровневые языки программирования (например, C) имеют низкоуровневые примитивы для управления памятью, такие как malloc() и free(). В JavaScript же память выделяется динамически при создании сущностей (т.е., объектов, строк и т.п.) и "автоматически" освобождается, когда они больше не используются. Последний процесс называется сборкой мусора . Слово "автоматически" является источником путаницы и зачастую создаёт у программистов на JavaScript (и других высокоуровневых языках) ложное ощущение, что они могут не заботиться об управлении памятью.

Жизненный цикл памяти

Независимо от языка программирования, жизненный цикл памяти практически всегда один и тот же:

  1. Выделение необходимой памяти.
  2. Её использование (чтение, запись).
  3. Освобождение выделенной памяти, когда в ней более нет необходимости.

Первые два пункта осуществляются явным образом (т.е., непосредственно программистом) во всех языках программирования. Третий пункт осуществляется явным образом в низкоуровневых языках, но в большинстве высокоуровневых языков, в том числе и в JavaScript, осуществляется автоматически.

Выделение памяти в JavaScript

Выделение памяти при инициализации значений переменных

Чтобы не утруждать программиста заботой о низкоуровневых операциях выделения памяти, интерпретатор JavaScript динамически выделяет необходимую память при объявлении переменных:

js
var n = 123; // выделяет память для типа number
var s = "azerty"; // выделяет память для типа string

var o = {
  a: 1,
  b: null,
}; // выделяет память для типа object и всех его внутренних переменных

var a = [1, null, "abra"]; // (like object) выделяет память для array и его внутренних значений

function f(a) {
  return a + 2;
} // выделяет память для function (которая представляет собой вызываемый объект)

// функциональные выражения также выделяют память под object
someElement.addEventListener(
  "click",
  function () {
    someElement.style.backgroundColor = "blue";
  },
  false,
);

Выделение памяти при вызовах функций

Вызовы некоторых функций также ведут к выделению памяти под объект:

js
var d = new Date();
var e = document.createElement("div"); // выделяет память под DOM-элемент

Некоторые методы выделяют память для новых значений или объектов:

js
var s = "azerty";
var s2 = s.substr(0, 3); // s2 это новый объект типа string
// Т.к. строки - это постоянные значения, интерпретатор может решить, что память выделять не нужно, но нужно лишь сохранить диапазон [0, 3].

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // новый массив с 4 элементами в результате конкатенации элементов 'a' и 'a2'

Использование значений

"Использование значений", как правило, означает - чтение и запись значений из/в выделенной для них области памяти. Это происходит при чтении или записи значения какой-либо переменной, или свойства объекта или даже при передаче аргумента функции.

Освобождение памяти, когда она более не нужна

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

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

Сборка мусора

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

Ссылки

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

В данном контексте понятие "объект" понимается несколько шире, нежели для типичных JavaScript-объектов и дополнительно включает в себя понятие областей видимости функций (или глобальной лексической области)

Сборка мусора на основе подсчёта ссылок

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

Пример

js
var o = {
  a: {
    b: 2,
  },
}; // создано 2 объекта. Один ссылается на другой как на одно из своих полей.
// Второй имеет виртуальную ссылку, поскольку присвоен в качестве значения переменной 'o'.
// Очевидно, что ни один из них не подлежит сборке мусора.

var o2 = o; // переменная 'o2' - вторая ссылка на объект
o = 1; // теперь объект, имевший изначально ссылку на себя из 'o' имеет уникальную ссылку через переменную 'o2'

var oa = o2.a; // ссылка на поле 'a' объекта.
// Теперь на объект 2 ссылки: одна на его поле и вторая - переменная 'oa'

o2 = "yo"; // Объект, на который изначально ссылалась переменная 'o', теперь имеет ноль ссылок на неё.
// Может быть уничтожен при сборке мусора.
// Однако, на его поле 'a' всё ещё ссылается переменная 'oa', так что удалять его ещё нельзя

oa = null; // оригинальное значение поля объекта 'a' в переменной o имеет ноль ссылок на себя.
// можно уничтожить при сборке мусора.

Ограничение : циклические ссылки

Основное ограничение данного наивного алгоритма заключается в том, что если два объекта ссылаются друг на друга (создавая таким образом циклическую ссылку), они не могут быть уничтожены сборщиком мусора, даже если "более не нужны".

js
function f() {
  var o = {};
  var o2 = {};
  o.a = o2; // o ссылается на o2
  o2.a = o; // o2 ссылается на o

  return "azerty";
}

f();

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

Пример из реальной жизни

Браузеры Internet Explorer версий 6, 7 имеют сборщик мусора для DOM-объектов, работающий по принципу подсчёта ссылок. Поэтому данные браузеры можно легко принудить к порождению систематических утечек памяти (memory leaks) следующим образом:

js
var div;
window.onload = function () {
  div = document.getElementById("myDivElement");
  div.circularReference = div; // DOM-элемент "myDivElement" получает ссылку на себя
  div.lotsOfData = new Array(10000).join("*");
};

DOM-элемент "myDivElement" имеет циклическую ссылку на самого себя в поле "circularReference". Если это свойство не будет явно удалено или установлено в null, сборщик мусора всегда будет определять хотя бы одну ссылку на DOM-элемент, и будет держать DOM-элемент в памяти, даже если DOM-элемент удалят из DOM-дерева. Таким образом, если DOM-элемент содержит много данных (иллюстрируется полем "lotsOfData"), то память, используемая под эти данные, никогда не будет освобождена.

Алгоритм "Mark-and-sweep"

Данный алгоритм сужает понятие "объект более не нужен" до "объект недоступен".

Основывается на понятии о наборе объектов, называемых roots (в JavaScript root'ом является глобальный объект). Сборщик мусора периодически запускается из этих roots, сначала находя все объекты, на которые есть ссылки из roots, затем все объекты, на которые есть ссылки из найденных и так далее. Стартуя из roots, сборщик мусора, таким образом, находит все доступные объекты и уничтожает недоступные.

Данный алгоритм лучше предыдущего, поскольку "ноль ссылок на объект" всегда входит в понятие "объект недоступен". Обратное же - неверно, как мы только что видели выше на примере циклических ссылок.

Начиная с 2012 года, все современные веб-браузеры оснащаются сборщиками мусора, работающими исключительно по принципу mark-and-sweep ("пометь и выброси"). Все усовершенствования в области сборки мусора в интерпретаторах JavaScript (генеалогическая/инкрементальная/конкурентная/параллельная сборка мусора) за последние несколько лет представляют собой усовершенствования данного алгоритма, но не новые алгоритмы сборки мусора, поскольку дальнейшее сужение понятия "объект более не нужен" не представляется возможным.

Теперь циклические ссылки - не проблема

В вышеприведённом первом примере после возврата из функции оба объекта не имеют на себя никаких ссылок, доступных из глобального объекта. Соответственно, сборщик мусора пометит их как недоступные и затем удалит.

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

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

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

Смотрите также