Почему 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. Он работает в два этапа:
- Mark (Пометка): GC начинает с корневых объектов (глобальный объект, текущие функции, локальные переменные). Он обходит все ссылки от корней и помечает каждый достижимый объект.
- 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 — не бесплатный. Он останавливает выполнение кода на время своей работы (так называемые «стоп-миры»). Если у вас много объектов в старом