# Управление памятью в JavaScript Управление памятью обычно незаметно. Мы создаём примитивы, объекты, функции.. Всё это занимает память. Что происходит с объектом, когда он становится "не нужен"? Возможно ли "переполнение" памяти? Для ответа на эти вопросы -- залезем "под капот" интерпретатора. [cut] ## Управление памятью в JavaScript Главной концепцией управления памятью в JavaScript является принцип *достижимости* (англ. reachability).
  1. Определённое множество значений считается достижимым изначально, в частности: Эти значения гарантированно хранятся в памяти. Мы будем называть их *корнями*.
  2. **Любое другое значение сохраняется в памяти лишь до тех пор, пока доступно из корня по ссылке или цепочке ссылок.**
Для очистки памяти от недостижимых значений в браузерах используется автоматический Сборщик мусора (англ. Garbage collection, GC), встроенный в интерпретатор, который наблюдает за объектами и время от времени удаляет недостижимые. Далее мы посмотрим ряд примеров, которые помогут в этом разобраться. ### Достижимость и наличие ссылок **Можно сказать просто: "значение остаётся в памяти, пока на него есть ссылка". Но такое упрощение будет не совсем верным.** ## Управление памятью в картинках **Рассмотрим пример объекта "семья":**
Код Структура в памяти
```js var family = { }; family.father = { name: "Вася" }; family.mother = { name: "Маша" }; ```
Этот код создаёт объект `family` и два дополнительных объекта, доступных по ссылкам `family.father` и `family.mother`. ### Недостижимый объект **Теперь посмотрим, что будет, если удалить ссылку `family.father` при помощи `delete`:**
Код Структура в памяти
```js var family = { }; family.father = { name: "Вася" }; family.mother = { name: "Маша" }; *!* delete family.father; */!* ```
### Пришёл сборщик мусора Сборщик мусора ищет недоступные объекты. Базовый алгоритм поиска -- это идти от корня (`window`) по ссылкам и помечать все объекты, которые встретит. Тогда после окончания обхода непомеченными останутся как раз недостижимые объекты. В нашем случае таким объектом будет бывший `family.father`. Он стал недостижимым и будет удалён вместе со своим "поддеревом", которое также более недоступно из программы.
Код Структура в памяти
```js var family = { father: { name: "Вася" }, mother: { name: "Маша" } }; *!* delete family.father; */!* ```
### После сборщика После того, как сработает сборщик мусора, картина в памяти будет такой:
Код Структура в памяти
```js var family = { father: { name: "Вася" }, mother: { name: "Маша" } }; *!* delete family.father; */!* ```
### Достижимость -- только по входящим ссылкам Вернёмся к исходному коду. **Пусть внутренние объекты ссылаются друг на друга:**
Код Структура в памяти
```js var family = { father: { name: "Вася" }, mother: { name: "Маша" } }; // добавим перекрёстных ссылок *!* family.father.wife = family.mother; family.mother.husband = family.father; family.father.we = family; family.mother.we = family; */!* ```
Получилась сложная структура, с круговыми ссылками. **Если удалить ссылки `family.father` и `family.mother.husband` (см. иллюстрацию ниже), то получится объект, который имеет исходящие ссылки, но не имеет входящих:**
Код Структура в памяти
```js var family = { father: { name: "Вася" }, mother: { name: "Маша" } }; family.father.wife = family.mother; family.mother.husband = family.father; family.father.we = family; family.mother.we = family; *!* delete family.father; delete family.mother.husband; */!* ```
При стандартном алгоритме очистки памяти, сборщик мусора пойдёт от корня и не сможет достичь объект, помеченный серым. Поэтому он будет удалён. **И совершенно неважно, что из объекта выходят какие-то ссылки `wife`, `we`, они не влияют на достижимость этого объекта.** ### Недостижимый остров Всё "семейство" объектов, которое мы рассматривали выше, достижимо исключительно через глобальную переменную `family` или, иными словами, через свойство `window.family`. Если записать в `window.family` что-то ещё, то все они, вместе со своими внутренними ссылками станут "недостижимым островом" и будут удалены:
Код Структура в памяти
```js var family = { father: { name: "Вася" }, mother: { name: "Маша" } }; family.father.wife = family.mother; family.mother.husband = family.father; family.father.we = family; family.mother.we = family; *!* family = null; */!* ```
## Замыкания Замыкания следуют тем же правилам, что и обычные объекты. **Объект переменных внешней функции существует в памяти до тех пор, пока существует хоть одна внутренняя функция, ссылающаяся на него через свойство `[[Scope]]`.** Например: ### Оптимизация в V8 и её последствия Современные JS-движки делают оптимизации замыканий по памяти. Они анализируют использование переменных и в случае, когда переменная из замыкания абсолютно точно не используется, удаляют её. В коде выше переменная `value` никак не используется. Поэтому она будет удалена из памяти. **Важный побочный эффект в V8 (Chrome, Opera) состоит в том, что удалённая переменная станет недоступна и при отладке!** Попробуйте запустить пример ниже с открытой консолью Chrome. Когда он остановится, в консоли наберите `alert(value)`. ```js //+ run function f() { var value = Math.random(); function g() { debugger; // выполните в консоли alert(value); Нет такой переменной! } return g; } var g = f(); g(); ``` Это может привести к забавным казусам при отладке, вплоть до того что вместо этой переменной будет другая, внешняя: ```js //+ run var value = "Сюрприз"; function f() { var value = "..."; function g() { debugger; // выполните в консоли alert(value); Сюрприз! } return g; } var g = f(); g(); ``` [warn header="Ещё увидимся"] Об этой особенности важно знать. Если вы отлаживаете под Chrome/Opera, то наверняка рано или поздно с ней встретитесь! Это не глюк отладчика, а особенность работы V8, которая, возможно, будет когда-нибудь изменена. Вы всегда сможете проверить, не изменилось ли чего, запустив примеры на этой странице. [/warn] ## Влияние управления памятью на скорость **На создание новых объектов и их удаление тратится время. Это важно иметь в виду в случае, когда важна производительность.** В качестве примера рассмотрим рекурсию. При вложенных вызовах каждый раз создаётся новый объект с переменными и помещается в стек. Потом память из-под него нужно очистить. Поэтому рекурсивный код будет всегда медленнее использующего цикл, но насколько? Пример ниже тестирует сложение чисел до данного через рекурсию по сравнению с обычным циклом: ```js //+ run function sumTo(n) { // обычный цикл 1+2+...+n var result = 0; for (var i=1; i<=n; i++) { result += i; } return result; } function sumToRec(n) { // рекурсия sumToRec(n) = n+SumToRec(n-1) return n == 1 ? 1 : n + sumToRec(n-1); } var timeLoop = performance.now(); for (var i=1;i<1000;i++) sumTo(1000); // цикл timeLoop = performance.now() - timeLoop; var timeRecursion = performance.now(); for (var i=1;i<1000;i++) sumToRec(1000); // рекурсия timeRecursion = performance.now() - timeRecursion; alert("Разница в " + ( timeRecursion / timeLoop ) + " раз"); ``` Различие в скорости на таком примере может составлять, в зависимости от интерпретатора, 2-10 раз. В большинстве ситуаций оптимизация по количеству создаваемых объектов несущественна, просто потому что "JavaScript и так достаточно быстр". Но она может быть важной для "узких мест" кода, а также при написании компьютерной графики и сложных вычислений на JS.