Как сборщик мусора находит неиспользуемые объекты в JavaScript
Что проверяют
C#-разработчику этот вопрос задают, когда в работе есть фронтенд, и заодно смотрят, видите ли вы параллели с .NET. Ждут слова «достижимость», а не «подсчёт ссылок». Следом спросят, откуда берутся утечки, если сборщик всё находит сам.
Как отвечать
Современные движки (V8 в Chrome и Node.js, SpiderMonkey, JavaScriptCore) используют трассирующую сборку, как и CLR. Объект жив, если до него можно дойти по ссылкам от корней: глобального объекта, стека вызовов, активных замыканий. Остальное мусор, и цикл, на который снаружи никто не ссылается, собирается без проблем.
Как это устроено в V8:
- молодое поколение: новые объекты живут в небольшой области, при сборке живые копируются (Scavenger), мёртвые исчезают даром. Расчёт на то, что большинство объектов умирает молодыми;
- старое поколение: сюда переезжают пережившие, чистка mark-sweep с уплотнением;
- паузы: разметка идёт инкрементально и в фоновых потоках (проект Orinoco), чтобы не замораживать интерфейс.
Параллель с .NET прямая: поколения 0, 1 и 2, отдельная куча больших объектов (LOH, от 85 000 байт), разметка от корней и уплотнение. Утечки в обоих мирах одной природы: объект не нужен, но достижим. В JS это забытые setInterval и обработчики событий, отсоединённые DOM-узлы в переменных, растущие кеши в Map. В C# то же даёт подписка на событие долгоживущего объекта без отписки.
Инструменты: WeakMap и WeakRef в JS, WeakReference<T> и ConditionalWeakTable в .NET. Диагностика в Chrome DevTools на вкладке Memory сравнением двух heap snapshot, в .NET через dotnet-gcdump и dotnet-counters.
Где спотыкаются
Называют подсчёт ссылок основным механизмом JS. Так было с DOM-объектами в старых версиях IE, современные движки так не работают.
Считают, что delete obj.prop или присвоение null сразу освобождают память. Они только убирают ссылку, а когда придёт сборщик, решает движок.
Думают, что замыкание держит только свои переменные. В V8 замыкания одной функции делят общий контекст, и большой объект, нужный одному обработчику, живёт, пока жив любой другой из той же функции.