Как работает JavaScript: управление памятью + обработка 4 распространенных утечек памяти

внешний интерфейс JavaScript Программа перевода самородков

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

Первая статья в этой серии посвящена предоставлениюОбзор движка, среда выполнения и вызовы стека. Вторая статья внимательно рассмотренаВнутренние блоки движка Google V8 JavaScriptИ дает несколько советов о том, как писать лучший код JavaScript.

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

Обзор

Например, такой язык программирования, как C, имеетmalloc()а такжеfree()Такая базовая функция управления памятью. Разработчики могут использовать эти функции для явного выделения и освобождения памяти операционной системы.

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

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

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

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

Ниже приводится обзор особенностей каждого шага в цикле:

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

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

  • освобождение памяти —  Освободите память, которая вам явно не нужна, чтобы снова сделать ее свободной и пригодной для использования. а такжевыделение памятиОпять же, в базовом языке это явная операция. Краткий обзор концепций стеков вызовов и куч памяти см.Первая статья по теме.

Что такое память?

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

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

Как люди, мы не очень хорошо реализуем все наши идеи и алгоритмы в битовых операциях, мы собираем их в более крупные группы, которые можно использовать для представления чисел. 8 бит называются 1 байтом. Кроме байтов есть слова (иногда 16, иногда 32 бита).

В памяти хранится многое:

  1. Все переменные и другие данные, используемые всеми программами.
  2. Программный код, включая код операционной системы.

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

Когда вы компилируете код, компилятор может проверить исходный тип данных и заранее рассчитать, сколько памяти ему нужно. Затем назначается необходимое количествопространство стекапрограмма в . Пространство, в котором размещаются эти переменные, называется пространством стека, потому что при вызове функций их память добавляется к существующей памяти. Когда они заканчиваются, они удаляются в порядке LIFO (последний вошел, первый вышел). Например, рассмотрим следующее утверждение:

int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes

Компилятор может сразу вычислить, что нужно коду

4 + 4 × 4 + 8 = 28 bytes

Вот как он обрабатывает текущий размер целых и двойных типов. Около 20 лет назад целые числа обычно занимали 2 байта, а числа типа double — 4 байта. Ваш код не должен зависеть от размера примитивных типов данных в один момент времени.

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

В приведенном выше примере компилятор знает конкретный адрес памяти каждой переменной. На самом деле, как только мы пишем в переменнуюn, он внутренне переводится как «адрес памяти 4127963».

Обратите внимание, что если мы попытаемся получить доступ сюдаx[4], мы получим доступ к данным, связанным с m . Это связано с тем, что мы обращаемся к несуществующему элементу массива — он длиннее, чем последний фактически выделенный элемент в массиве.x[3]на 4 байта глубже и может в конечном итоге прочитать (или перезаписать) некоторыеmкусочек. Это имеет непредвиденные последствия для остальной части проекта.

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

динамическое размещение

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

int n = readInput(); // reads input from the user
...
// create an array with "n" elements

Здесь, во время компиляции, компилятор не знает, сколько памяти нужно массиву, потому что это определяется значением, введенным пользователем.

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

Разница между статическим и динамическим выделением памяти

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

Распределение памяти в JavaScript

Теперь мы объясним первый шаг (Выделить память) работает в JavaScript.

JavaScript освобождает разработчика от ответственности за выделение памяти — JavaScript сам выполняет выделение памяти, объявляя значение.

var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string 
var o = {
  a: 1,
  b: null
}; // allocates memory for an object and its contained values
var a = [1, null, 'str'];  // (like object) allocates memory for the
                           // array and its contained values
function f(a) {
  return a + 3;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

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

var d = new Date(); // allocates a Date object

var e = document.createElement('div'); // allocates a DOM element

Методы могут присваивать новые значения или объекты:

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable, 
// JavaScript may decide to not allocate memory, 
// but just store the [0, 3] range.
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2); 
// new array with 4 elements being
// the concatenation of a1 and a2 elements

Работа с памятью в JavaScript

По сути, использование выделенной памяти в JavaScript означает чтение и запись в нее.

Это можно сделать, прочитав или записав значение переменной или свойства объекта или даже передав переменную функции.

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

Подавляющее большинство проблем с управлением памятью находится на этом этапе.

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

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

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

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

механизм сбора мусора

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

ссылка на память

Основные концепции, на которые опираются алгоритмы сборки мусора, исходят изПриложение Ссылки.

В контексте управления памятью говорят, что объект ссылается на другой объект, если он имеет доступ к другому объекту (неявно или явно). Например, JavaScript, который ссылается на свойprototype (неявная ссылка) и значение его атрибута (явная ссылка).

В этом контексте понятие «объект» распространяется на более широкую область, чем обычные объекты JavaScript, и включает в себя область действия (или глобальнуюЛексическая область).

Лексическая область определяет, как имена переменных разрешаются во вложенных функциях: внутренняя функция по-прежнему содержит область действия родительской функции, даже если родительская функция возвратила значение.

сборка мусора с подсчетом ссылок

Это простейший алгоритм сборки мусора. Если естьноль баллов на это, объект считается "мусоро-коллекционным".

См. код ниже:

var o1 = {
  o2: {
    x: 1
  }
};
// 2 objects are created. 
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected

var o3 = o1; // the 'o3' variable is the second thing that 
            // has a reference to the object pointed by 'o1'. 
                                                       
o1 = 1;      // now, the object that was originally in 'o1' has a         
            // single reference, embodied by the 'o3' variable

var o4 = o3.o2; // reference to 'o2' property of the object.
                // This object has now 2 references: one as
                // a property. 
                // The other as the 'o4' variable

o3 = '374'; // The object that was originally in 'o1' has now zero
            // references to it. 
            // It can be garbage-collected.
            // However, what was its 'o2' property is still
            // referenced by the 'o4' variable, so it cannot be
            // freed.

o4 = null; // what was the 'o2' property of the object originally in
           // 'o1' has zero references to it. 
           // It can be garbage collected.

проблемы с циклом

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

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}

f();

Алгоритмы маркировки и сканирования

Чтобы определить, нужен ли объект, алгоритм определяет, доступен ли объект.

Алгоритм маркировки и сканирования проходит следующие 3 этапа:

1. Корневой узел. Вообще говоря, корень — это глобальная переменная, на которую ссылается код. Например, в JavaScript глобальная переменная, которая может действовать как корневой узел, — это объект «окно». Глобальный объект в Node.js называется «глобальным». Полный список корневых узлов создается сборщиком мусора. 2. Затем алгоритм проверяет все корневые узлы и их дочерние элементы и помечает их как активные (то есть они не являются мусором). Любая переменная, недоступная для корневого узла, будет помечена как мусор. 3. Наконец, сборщик мусора освобождает все блоки памяти, не помеченные как активные, и возвращает эту память операционной системе.

Визуализация поведения алгоритма маркировки и сканирования.

Поскольку «объект не имеет ссылок» делает объект недостижимым, этот алгоритм лучше предыдущего. То, что мы видим в цикле, как раз наоборот, что не соответствует действительности.

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

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

Циклы больше не проблема

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

Даже если есть ссылка между двумя объектами, они больше не доступны из корневого узла.

Статистика интуитивного поведения сборщика мусора

Какими бы удобными ни были сборщики мусора, у них также есть свой набор компромиссов. Один из них — неопределенность. Другими словами, GC (сборщики мусора) непредсказуемы. Вы не можете быть уверены, когда сборщик мусора выполнит сборку. Это означает, что в некоторых случаях программе действительно нужно использовать больше памяти. В других случаях в особо чувствительных приложениях могут быть заметны короткие паузы. Большинство сборщиков мусора используют общий шаблон сборки мусора при выделении памяти, хотя недетерминизм означает, что нельзя определить, когда сборщик мусора будет выполнять сборку. Если никакие распределения не выполняются, большинство GC остаются бездействующими. Рассмотрим следующий сценарий:

  1. Выполняется большое количество выделений.
  2. Большинство (или все) этих элементов помечаются как недоступные (при условии, что мы уничтожаем ссылку на кеш, который нам больше не нужен).
  3. Более глубокое выделение памяти не выполняется.

В этом случае большинство сборщиков мусора не будут запускать более глубокие коллекции. Другими словами, даже если для сбора доступны неиспользуемые ссылки, сборщик не будет объявлять эти ссылки. Это, строго говоря, не утечки, но они все же приводят к большему, чем повседневное использование памяти.

Что такое утечка памяти?

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

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

Некоторые языки программирования предоставляют функции, помогающие разработчикам делать вышеперечисленное. Другие хотят, чтобы разработчик был полностью свободен, когда часть памяти не используется. как википедияруководствоа такжеавтоматическийЕсть хорошие статьи по управлению памятью.

Четыре распространенные утечки памяти в JavaScript

1: глобальная переменная

JavaScript обрабатывает необъявленные переменные интересным способом: при ссылке на необъявленную переменнуюglobalСоздайте новую переменную в объекте. В браузере глобальный объект будетwindow,это означает

function foo(arg) {
    bar = "some text";
}

Эквивалентно:

function foo(arg) {
    window.bar = "some text";
}

давайте предположимbarЦель состоит в том, чтобы просто сослаться на переменную в функции foo. Однако, если вы не используетеvarЧтобы объявить его, он создает избыточную глобальную переменную. В вышеописанном случае это не будет иметь очень серьезных последствий. Можно представить себе более разрушительный сценарий.

Вы также можете использоватьthisСлучайно была создана глобальная переменная:

function foo() {
    this.var1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

Вы можете сделать это, добавив в начало файла JavaScript'use strict';Чтобы избежать этих последствий, включается более строгий режим синтаксического анализа JavaScript, который предотвращает случайное создание глобальных переменных.

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

2: Забытый таймер или функция обратного вызова

Начнем с тех, которые часто используются в JavaScript.setIntervalНапример.

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

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.

Фрагмент кода выше показывает последствия использования таймера для ссылки на узел или бесполезные данные.

rendererОбъект может быть удален или заменен в какой-то момент, что сделает интервал блокировки обработчика пакета избыточным. Если это произойдет, обработчик и его зависимости не будут собраны, потому что вам нужно подготовиться к остановке интервала обработки (помните, что он все еще активен). Все сводится к тому, что факты хранения и обработки данных загружаютserverDataЕго тоже не соберут.

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

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

Но, тем не менее, рекомендуется удалять наблюдателя, как только объект устареет. См. пример ниже:

var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers // that don't handle cycles well.

Браузеры теперь поддерживают сборщики мусора, которые обнаруживают эти циклы и обрабатывают их соответствующим образом, поэтому вам больше не нужно вызывать перед созданием недостижимого узла.removeEventListener.

если вы используетеjQueryAPI (другие библиотеки и фреймворки также поддерживают это), вы также можете удалить прослушиватели до того, как node станет устаревшим. Эти библиотеки гарантируют отсутствие утечек памяти, даже если приложение работает в старых версиях браузера.

3: Закрытие

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

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // a reference to 'originalThing'
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

Однажды позвонилreplaceThingфункция,theThingвы получаете новый объект, состоящий из большого массива и нового замыкания (someMethod)сочинение. ОднакоoriginalThingпоunusedпеременная (это из предыдущего вызоваreplaceThingПеременнаяThingпеременная) ссылается на удерживаемое ею замыкание. Что помнить, этоКак только область создается для замыкания в той же родительской области, область действия становится общей.

В одном примереsomeMethodСоздал область сunusedобщий.unusedсодержит оoriginalThingцитаты. хотяunusedникогда не цитировался,someMethodтакже черезreplaceThingвыходит за рамкиtheThingиспользовать его (например, где-то глобально). из-заsomeMethodа такжеunusedобщая область закрытия,unusedнаправлениеoriginalThingСсылка заставляет его оставаться в живых (вся общая область между двумя замыканиями). Это предотвращает их сбор мусора.

В приведенном выше примере замыканиеsomeMethodСоздал область сunusedобщий, иunusedцитировать сноваoriginalThing.someMethodв состоянии пройтиreplaceThingвыходит за рамкиtheThingцитировать, хотяunusedникогда не цитировался. На самом деле неиспользованныйoriginalThingСсылка требует, чтобы она оставалась активной, потому чтоsomeMethodОбщая объемлющая область с неиспользуемыми.

Все это может привести к массовым утечкам памяти. Когда приведенный выше фрагмент выполняется снова и снова, вы можете ожидать увеличения использования памяти. Его размер не уменьшается при запуске сборщика мусора. Создается цепочка замыкания (в примере ее кореньtheThingпеременная), и каждая область замыкания содержит косвенную ссылку на большой массив.

Команда Meteor обнаружила эту проблему,у них отличная статьяЭта проблема подробно описана.

4: Ссылки за пределы DOM

В некоторых случаях разработчики хранят узлы DOM в структурах данных. Предположим, вы хотите быстро обновить содержимое нескольких строк в таблице. Если вы сохраните ссылку на каждую строку DOM в словаре или массиве, у вас будет две ссылки на один и тот же элемент DOM: одна в дереве DOM и одна в словаре. Если вы решите удалить эти строки, вам нужно не забыть сделать обе ссылки недоступными.

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    // The image is a direct child of the body element.
    document.body.removeChild(document.getElementById('image'));
    // At this point, we still have a reference to #button in the
    //global elements object. In other words, the button element is
    //still in memory and cannot be collected by the GC.
}

Существует дополнительный фактор, который следует учитывать при работе с внутренними или конечными узлами дерева DOM. Если вы сохраните ячейки таблицы в своем коде (tdтег) и решает удалить таблицу из DOM, но сохранить ссылку на эту конкретную ячейку, вы можете ожидать серьезной утечки памяти. Вы можете подумать, что сборщик мусора освободит все, кроме этой ячейки. Но это не так. Поскольку ячейка является дочерним узлом таблицы, а дочерний узел поддерживает ссылку на родительский узел, поэтомуЭта единственная ссылка на ячейку таблицы сохраняет всю таблицу в памяти..

мы вSessionStackПопробуйте следовать этим рекомендациям и напишите код, который правильно обрабатывает выделение памяти по следующим причинам:

Как только SessionStack интегрирован в ваше рабочее веб-приложение, он начинает регистрировать все: все изменения DOM, взаимодействия с пользователем, исключения JavaScript, трассировку стека, неудачные сетевые запросы, отладочные сообщения и т. д.

С помощью SessionStack вы можете воспроизводить проблемы в своем веб-приложении, как видео, и видеть все поведение пользователя. Все это должно быть сделано без снижения производительности вашего веб-приложения.

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

Вот бесплатный план, чтобы вы моглипопытайся.

Resources


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,React,внешний интерфейс,задняя часть,товар,дизайнЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.