Управление памятью и сборщик мусора в JavaScript: как это работает

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему JavaScript сам чистит за нами, но мы всё равно должны понимать, как это работает?

Когда вы пишете на JavaScript, вы редко задумываетесь о том, что происходит с переменными после того, как они вышли из области видимости. В отличие от C или C++, где вы обязаны вручную вызывать free() или delete, JavaScript берёт на себя грязную работу по очистке памяти. Это удобно — пока не наступает момент, когда ваше приложение начинает «тормозить», потреблять гигабайты RAM или внезапно падать с ошибкой Out of Memory.

Знакомо? Тогда давайте разберёмся, как на самом деле работает управление памятью в JavaScript и что такое сборщик мусора JavaScript. Спойлер: это не магия, а строгий алгоритм, который можно (и нужно) учитывать при написании кода.

Как JavaScript хранит данные: стек и куча

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

  • Стек (Stack) — быстрая, но ограниченная память. Там хранятся примитивы (числа, строки, булевы значения) и ссылки на объекты. Каждый вызов функции создаёт новый фрейм в стеке.
  • Куча (Heap) — неструктурированная область памяти, где живут объекты, массивы, функции и замыкания. Именно здесь происходит основная борьба за память.

Когда вы создаёте объект:

let user = { name: 'Иван', age: 30 };

Ссылка user попадает в стек, а сам объект { name: 'Иван', age: 30 } — в кучу. Пока на объект есть хотя бы одна ссылка, он жив. Как только ссылок не остаётся, объект становится «мусором».

Сборщик мусора в JavaScript: главный герой

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

Алгоритм Mark-and-Sweep (Пометка и зачистка)

Современные движки (V8 в Chrome, SpiderMonkey в Firefox) используют алгоритм Mark-and-Sweep. Он работает в два этапа:

  1. Mark (Пометка): GC начинает с корневых объектов (глобальный объект, текущие функции, локальные переменные). Он обходит все ссылки от корней и помечает каждый достижимый объект.
  2. Sweep (Зачистка): Все непомеченные объекты считаются мусором и удаляются. Память освобождается.

Пример на пальцах:

let a = { data: 'A' };
let b = { data: 'B' };
a.ref = b;  // a ссылается на b
b.ref = a;  // b ссылается на a

// Теперь у нас циклическая ссылка
a = null;   // a больше не указывает на объект
b = null;   // b больше не указывает на объект

Раньше (до появления Mark-and-Sweep) такие циклические ссылки приводили к утечкам. Но современный GC понимает: если оба объекта недостижимы из корня, они — мусор, даже если ссылаются друг на друга.

Поколения объектов: молодые и старые

Большинство объектов живут недолго. Например, временные переменные внутри функции. Поэтому V8 делит кучу на поколения:

  • Young Generation (Молодое поколение): новые объекты. Сборка здесь происходит часто и быстро (Scavenge — алгоритм, копирующий живые объекты в другую область).
  • Old Generation (Старое поколение): объекты, пережившие несколько сборок. Их проверяют реже, но процесс занимает больше времени.

Этот подход позволяет не «замораживать» страницу на время полной проверки всей кучи.

Утечки памяти в JavaScript: как их допустить (и не надо)

Даже с умным сборщиком мусора можно создать ситуацию, когда память не освобождается. Вот самые частые причины утечек памяти в JavaScript:

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

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

function leak() {
    leakedVar = 'Я утечка!'; // без let/const/var — глобальная
}

Решение: всегда используйте let, const или 'use strict'.

2. Забытые таймеры и обработчики событий

function startTimer() {
    let bigData = new Array(1000000).fill('*');
    setInterval(() => {
        console.log(bigData.length);
    }, 1000);
}
// bigData никогда не освободится, пока жив setInterval

Решение: сохраняйте идентификатор таймера и очищайте его clearInterval().

3. Замыкания, удерживающие большие объекты

function createClosure() {
    let heavy = new Array(1000000).fill('data');
    return function() {
        console.log('Hello');
        // heavy остаётся в памяти, даже если не используется
    };
}

Решение: если переменная не нужна внутри замыкания, обнулите её (heavy = null).

4. DOM-элементы, удалённые, но не очищенные

let btn = document.getElementById('myButton');
btn.addEventListener('click', () => {
    document.body.removeChild(btn);
    // btn всё ещё жив в переменной и в памяти
});

Решение: после удаления элемента обнулите ссылку или используйте removeEventListener.

Слабые ссылки: как помочь сборщику мусора

Иногда вы хотите хранить ссылку на объект, но не препятствовать его сборке. Для этого в JavaScript есть слабые ссылки — WeakMap и WeakSet.

Пример: кеширование данных, которое не должно блокировать очистку:

let cache = new WeakMap();

function process(obj) {
    if (!cache.has(obj)) {
        let result = expensiveComputation(obj);
        cache.set(obj, result);
    }
    return cache.get(obj);
}
// Когда obj удаляется из внешнего кода, запись в WeakMap тоже исчезает

Используйте WeakMap для хранения метаданных, связанных с объектами, которые вы не контролируете (например, DOM-элементы).

Влияние сборщика мусора на производительность

Сборщик мусора JavaScript — не бесплатный. Он останавливает выполнение кода на время своей работы (так называемые «стоп-миры»). Если у вас много объектов в старом

Блоги

Рекомендаций по книгам