Урок по треку JavaScript 6 мин

Память и сборка мусора за 6 минут

Достижимость, mark-and-sweep, типичные утечки, WeakMap и WeakRef.

В JavaScript память не освобождают вручную: этим занимается сборщик мусора (garbage collector, GC). Но утечки всё равно случаются, и в долгоживущих SPA и Node-серверах они превращаются в тормоза и падения. На собесе проверяют, понимаете ли вы, что держит объект в памяти.

Где живут данные

  • Стек: кадры вызовов функций, локальные примитивы. Освобождается автоматически при выходе из функции.
  • Куча (heap): объекты, массивы, функции, замыкания. Ими и управляет GC.

Достижимость

Объект живёт, пока он достижим из корней. Корни это глобальный объект, текущий стек вызовов (локальные переменные и аргументы), активные замыкания, обработчики, на которые ссылается окружение (DOM, таймеры).

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

Mark-and-sweep

Основной алгоритм всех современных движков:

  1. Mark: обойти граф от корней и пометить всё достижимое.
  2. Sweep: освободить непомеченное.
  3. Иногда compact: уплотнить оставшиеся объекты, чтобы не было фрагментации.

Старый подход с подсчётом ссылок не справлялся с циклами, поэтому от него ушли.

Поколения в V8

Большинство объектов умирают молодыми, поэтому куча V8 разделена:

  • Молодое поколение (young generation): новые объекты, частые и быстрые сборки (Scavenger, копирующий сборщик).
  • Старое поколение (old generation): объекты, пережившие пару сборок; реже, mark-sweep-compact.

Большая часть работы делается параллельно и инкрементально (сборщик Orinoco), но паузы всё равно бывают, и частое создание объектов в горячем цикле им способствует.

Типичные утечки во фронтенде

  1. Забытые таймеры и интервалы. setInterval, который не очистили при размонтировании, держит колбэк и всё, что он замыкает.
  2. Подписки и слушатели. window.addEventListener без removeEventListener, подписка на стор или WebSocket без отписки.
  3. Отсоединённые DOM-узлы (detached). Элемент удалили из документа, но ссылка на него осталась в массиве, Map или замыкании.
  4. Растущие кеши и коллекции. Map как кеш без ограничения размера.
  5. Замыкания, удерживающие большие данные. Обработчик держит ссылку на огромный массив, хотя использует одно поле.
  6. Глобальные переменные. Случайная глобальная переменная в нестрогом режиме живёт вечно.

В React большинство утечек лечится функцией очистки эффекта:

useEffect(() => {
  const id = setInterval(tick, 1000);
  const onResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', onResize);
  return () => {
    clearInterval(id);
    window.removeEventListener('resize', onResize);
  };
}, []);

Для слушателей удобно AbortController: addEventListener('resize', fn, { signal }) и один abort() снимает все подписки.

Слабые ссылки

  • WeakMap / WeakSet: ключи не удерживаются. Когда объект-ключ стал недостижим, запись исчезнет. Нет size и итерации, потому что содержимое зависит от момента сборки.
  • WeakRef: слабая ссылка на объект, ref.deref() вернёт объект или undefined, если его уже собрали.
  • FinalizationRegistry: колбэк после сборки объекта. Момент вызова не гарантирован, вызова может не быть вовсе, поэтому строить на нём логику нельзя, только вспомогательную очистку.

Как искать утечки

Chrome DevTools, вкладка Memory:

  • Heap snapshot: сделать снимок, повторить действие несколько раз, сделать второй и сравнить (Comparison). Фильтр Detached показывает отсоединённые DOM-узлы, а панель Retainers показывает цепочку, которая держит объект.
  • Allocation instrumentation on timeline: какие объекты создаются и не освобождаются.
  • Вкладка Performance с галочкой Memory: «пила» это норма, ступенчатый рост после сборок признак утечки.

В Node.js: process.memoryUsage(), флаг --inspect и те же снимки кучи, --max-old-space-size для лимита.

Частые ошибки

  • Думать, что delete obj.prop или x = null немедленно освобождают память. Они лишь убирают ссылку, освободит сборщик, когда придёт время.
  • Считать циклические ссылки утечкой.
  • Использовать WeakRef для обычной логики приложения.
  • Не отписываться от событий в компонентах.

Что спросят на собесе

  • Как работает сборка мусора в JS, что такое достижимость.
  • Какие утечки памяти бывают во фронтенде и как их найти.
  • Чем WeakMap отличается от Map, где его применить.
  • Как очищать таймеры и подписки в React.
  • Утекут ли объекты с циклическими ссылками.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.