Вам бонус- начислено 1 монета за дневную активность. Сейчас у вас 1 монета

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

Лекция



Привет, сегодня поговорим про управление памятью в js, обещаю рассказать все что знаю. Для того чтобы лучше понимать что такое управление памятью в js , настоятельно рекомендую прочитать все из категории Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend).

Введение

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


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

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

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

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

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

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

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

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

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

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);

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

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

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

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

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-объектов и дополнительно включает в себя понятие областей видимости функций (или глобальной лексической области)

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

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

Пример

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 имеет ноль ссылок на себя.
// можно уничтожить при сборке мусора.

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

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

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) следующим образом:

var div = document.createElement("div");
div.onclick = function(){
  doSomething();
}; // div имеет ссылку на обработчик события через свойство 'onclick'.
// Обработчик также ссылается на div, поскольку переменная 'div' доступна из области видимости функции.
// Таким образом, оба объекта не могут быть уничтожены сборщиком мусора и порождают утечку памяти.

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

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

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

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

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

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

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

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

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

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

В JavaScript, где не надо вручную освобождать память, реализована технология, называемая сборкой мусора (garbage collection). Интерпретатор JavaScript может обнаружить, что объект никогда более не будет использоваться программой. Определив, что объект недоступен (т. е. больше нет способа получения ссылки на него), интерпретатор выясняет, что объект более не нужен, и занятая им память может быть освобождена.2 Рассмотрим следующие строки кода:

var s = "hello"; // Выделяем память для строки

var u = s.toUpperCase(); // Создаем новую строку

s = u; // Переписываем ссылку на первоначальную строку

После работы этого кода исходная строка "hello" больше недоступна – ни в одной из переменных программы нет ссылки на нее. Система определяет этот факт и освобождает память. Сборка мусора выполняется автоматически и невидима для программиста. О сборке мусора он должен знать ровно столько, сколько ему требуется, чтобы доверять ее работе, – он не должен думать, куда делись все старые объекты.


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


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

Управление памятью в JS
Жизненный цикл памяти: выделение, использование, освобождение

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


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

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


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

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

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

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

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


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

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

int n; // 4 байта
int x[4]; // массив из 4-х элементов по 4 байта каждый
double m; // 8 байтов


Компилятор, просмотрев данный фрагмент кода (абстрагируемся тут от всего, кроме размеров самих данных), может немедленно выяснить, что для хранения переменных понадобится 4 + 4 × 4 + 8 = 28 байт.

Надо отметить, что приведенные размеры целочисленных переменных и чисел с двойной точностью отражают современное состояние дел. Об этом говорит сайт https://intellect.icu . Примерно 20 лет назад целые числа обычно представляли в виде 2-х байтовых конструкций, для чисел двойной точности использовали 4 байта. Код не должен зависеть от байтовых размеров базовых типов данных.

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

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

Обратите внимание на то, что если мы попытаемся обратиться к элементу массива из нашего примера, использовав конструкцию x , мы, на самом деле, обратимся к данным, которые соответствуют переменной m. Так происходит из-за того, что элемента массива с индексом 4 не существует, запись вида x укажет на область памяти, которая на 4 байта дальше, чем тот участок памяти, который выделен для последнего из элементов массива — x . Попытка обращения к x может закончится чтением (или перезаписью) некоторых битов переменной m. Подобное, практически гарантированно, приведет к нежелательным последствиям в ходе выполнения программы.

Управление памятью в JS
Расположение переменных в памяти

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

Динамическое выделение памяти


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

int n = readInput(); // прочесть данные, введенные пользователем
...
// создать массив с n элементами


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

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

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

Статическое выделение памяти Динамическое выделение памяти
Объем должен быть известен во время компиляции. Объем может быть неизвестен во время компиляции.
Производится во время компиляции программы. Производится во время выполнения программы.
Память выделяется в стеке. Память выделяется в куче
Порядок выделения памяти FILO (первым вошел — последним вышел, First In Last Out) Определенного порядка выделения памяти нет.


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

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


Сейчас мы поговорим о том, как первый шаг жизненного цикла памяти (выделение) реализуется в JavaScript.

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

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

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

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


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

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

Использование памяти в JavaScript


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

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

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


Большинство проблем с управлением памятью возникает на этой стадии.

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

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

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

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

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


Основная концепция, на которую полагаются алгоритмы сборки мусора — это концепция ссылок.

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

Здесь идея «объекта» расширяется до чего-то большего, нежели обычный JS-объект, сюда включаются, кроме того, функциональные области видимости (или глобальную лексическую область видимости).

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

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


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

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

Циклические ссылки — источник проблем


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

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

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


Циклическая ссылка

Алгоритм «пометь и выброси»


Для того, чтобы принять решение о том, нужно ли сохранить некий объект, алгоритм «пометь и выброси» (mark and sweep) определяет досягаемость объекта.
Алгоритм состоит из следующих шагов:

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

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

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

Визуализация алгоритма «пометь и выброси»

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

С 2012-го года все современные браузеры оснащают сборщиками мусора, в основу которых положен алгоритм «пометь и выброси». За последние годы все усовершенствования, сделанные в сфере сборки мусора в JavaScript (это — генеалогическая, инкрементальная, конкурентная, параллельная сборка мусора), являются усовершенствованиями данного алгоритма, не меняя его основных принципов, которые заключаются в определении достижимости объекта.

В этом материале вы можете найти подробности о рассматриваемой здесь модели сборки мусора.

Решение проблемы циклических ссылок


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

Управление памятью в JS
Циклические ссылки не мешают сборке мусора

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

Парадоксальное поведение сборщиков мусора


Хотя сборщики мусора удобны, при их использовании приходится идти на определенные компромиссы. Один из них — недетерминированность. Другими словами, сборщики мусора непредсказуемы. Нельзя точно сказать, когда будет выполнена сборка мусора. Это означает, что в некоторых случаях программы используют больше памяти, чем им на самом деле нужно.

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

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


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

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


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

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


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

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

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

Утечки памяти в JavaScript и борьба с ними

▍Глобальные переменные


В JavaScript используется интересный подход к работе с необъявленными переменными. Обращение к такой переменной создает новую переменную в глобальном объекте. В случае с браузерами, глобальным объектом является 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";
}
// Функция вызывается сама по себе, при этом this указывает на глобальный объект (window),
// this не равно undefined, или, как при вызове конструктора, не указывает на новый объект
foo();


Для того, чтобы избежать подобных ошибок, можно добавить оператор "use strict"; в начало JS-файла. Это включит так называемый строгий режим, в котором запрещено создание глобальных переменных вышеописанными способами. Подробнее о строгом режиме можно почитать здесь.

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

▍Таймеры или забытые коллбэки


В JS-программах использование функции setInterval — обычное явление.

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

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //Это будет вызываться примерно каждые 5 секунд.


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

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

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

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

var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// Сделать что-нибудь
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Теперь, когда элемент выходит за пределы области видимости,
// память, занятая обоими элементами и обработчиком onClick будет освобождена даже в старых браузерах,
// которые не способны нормально обрабатывать ситуации с циклическими ссылками.


В наши дни браузеры (в том числе Internet Explorer и Microsoft Edge) используют современные алгоритмы сборки мусора, которые выявляют циклические ссылки и работают с соответствующими объектами правильно. Другими словами, сейчас нет острой необходимости в использовании метода removeEventListener перед тем, как узел будет сделан недоступным.

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

▍Замыкания


Одна из важных и широко используемых возможностей JavaScript — замыкания. Это — внутренняя функция, у которой есть доступ к переменным, объявленным во внешней по отношению к ней функции. Особенности реализации среды выполнения JavaScript делают возможной утечку памяти в следующем сценарии:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // ссылка на originalThing
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);


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

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

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

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

▍Ссылки на объекты DOM за пределами дерева DOM


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

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
function doStuff() {
    image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    // Изображение является прямым потомком элемента body.
    document.body.removeChild(document.getElementById('image'));
    // В данный момент у нас есть ссылка на #button в
    // глобальном объекте elements. Другими словами, элемент button
    // все еще хранится в памяти, она не может быть очищена сборщиком мусора.
}


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

Предположим, мы храним ссылку на конкретную ячейку таблицы (тег <td>) в JS-коде. Через некоторое время решено убрать таблицу из DOM, но сохранить ссылку на эту ячейку. Чисто интуитивно можно предположить, что сборщик мусора освободит всю память, выделенную под таблицу, за исключением памяти, выделенной под ячейку, на которую у нас есть ссылка В реальности же все не так. Ячейка является узлом-потомком таблицы. Потомки хранят ссылки на родительские объекты. Таким образом, наличие ссылки на ячейку таблицы в коде приводит к тому, что в памяти остается вся таблица. Учитывайте эту особенность, храня ссылки на элементы DOM в программах.

Итоги


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

Уважаемые читатели! Сталкивались ли вы с утечками памяти в JavaScript-программах? Если да — расскажите пожалуйста, как это было, и как вы с этим справились.

Вау!! 😲 Ты еще не читал? Это зря!

К сожалению, в одной статье не просто дать все знания про управление памятью в js. Но я - старался. Если ты проявишь интерес к раскрытию подробностей,я обязательно напишу продолжение! Надеюсь, что теперь ты понял что такое управление памятью в js и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)

Ответы на вопросы для самопроверки пишите в комментариях, мы проверим, или же задавайте свой вопрос по данной теме.

создано: 2014-10-07
обновлено: 2024-11-14
248



Рейтиг 9 of 10. count vote: 2
Вы довольны ?:


Поделиться:

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выплнения задания
  • Возможно примерение функции гаранта на сделку
  • Приорететная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно примерение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии


Оставить комментарий
Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)

Термины: Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)