Как сборщик мусора находит неиспользуемые объекты в JavaScript
Что проверяют
Понимаете ли вы, что сборщик мусора ищет не «ненужные», а недостижимые объекты, и чем этот подход лучше подсчёта ссылок. Следом почти всегда просят назвать типичные утечки памяти во фронтенде и показать, как их найти в DevTools.
Как отвечать
Опорное слово здесь «достижимость». Объект живёт, пока до него можно дойти по цепочке ссылок от корней:
- корни: глобальный объект, текущий стек вызовов с локальными переменными, переменные в замыканиях живых функций, а в браузере ещё DOM-дерево и зарегистрированные обработчики;
- mark-and-sweep: сборщик обходит граф от корней, помечает всё, до чего дотянулся, и освобождает остальное. Циклы ему не мешают: два объекта, ссылающихся друг на друга, но недостижимых от корней, будут собраны;
- подсчёт ссылок: упоминайте как контраст. На нём споткнулись старые IE, где цикл между DOM-узлом и JS-объектом утекал навсегда. Современные движки для JS-кучи его не используют;
- поколения: V8 делит кучу на молодое и старое поколение. Молодое чистится часто и дёшево (Scavenger копирует выживших), старое реже, полной разметкой с уплотнением;
- короткие паузы: разметка идёт инкрементально и частично в фоновых потоках.
Закончите тем, что управлять сборкой из кода нельзя: obj = null или delete только убирают ссылку, а когда придёт сборщик, решает движок. Для кэшей, которые не должны удерживать объекты, есть WeakMap, WeakSet и WeakRef, а на момент срабатывания FinalizationRegistry полагаться нельзя.
Где спотыкаются
Называют подсчёт ссылок основным механизмом современных браузеров и не могут объяснить, почему тогда циклические ссылки не утекают.
Уверены, что присваивание null мгновенно освобождает память. Оно лишь делает объект кандидатом на сборку, и только если на него больше никто не ссылается.
Не могут привести ни одной реальной утечки. Типичные: обработчик через addEventListener без снятия, setInterval или подписка в useEffect без функции очистки, отсоединённый DOM-узел, который держит переменная, бесконечно растущий кэш в модуле. Ищут их сравнением двух снимков кучи во вкладке Memory, до и после действия.