Как устроен garbage collector
Что проверяют
Понимаете ли вы, как V8 решает, что объект можно удалить, и почему сборка по поколениям даёт короткие паузы. Следом почти всегда спрашивают, как вы искали утечку памяти в Node.js.
Как отвечать
Объект живёт, пока достижим от корней: глобального объекта, стека, активных замыканий и хендлов. Счётчика ссылок в V8 нет, поэтому циклические ссылки сборке не мешают.
Куча V8 делится на поколения, потому что большинство объектов живёт недолго:
- молодое поколение: небольшое пространство из двух полупространств. Minor GC (Scavenger) копирует живые объекты в свободную половину, мёртвые не трогает вовсе, поэтому пауза пропорциональна числу выживших. Пережившие две сборки переезжают в старое поколение;
- старое поколение: Mark-Sweep-Compact. Разметка обходит граф от корней, неотмеченное освобождается, фрагментированные страницы уплотняются;
- Orinoco: разметка идёт инкрементально и параллельно с JavaScript в фоновых потоках, очистка тоже фоновая, поэтому основная пауза остаётся короткой.
Практика в Node.js:
- лимит:
--max-old-space-sizeв мегабайтах; значение по умолчанию зависит от версии и доступной памяти, в контейнере его лучше задавать явно; - наблюдение:
--trace-gc,process.memoryUsage(),v8.getHeapStatistics(); - поиск утечек:
node --inspectи снимки кучи в Chrome DevTools, сравнение двух снимков,--heapsnapshot-near-heap-limit, чтобы получить снимок перед падением.
Buffer и ArrayBuffer хранят данные вне кучи V8, это видно в полях external и arrayBuffers, и --max-old-space-size их не ограничивает.
Где спотыкаются
Говорят, что GC работает на подсчёте ссылок, и объясняют утечки циклами. В V8 утекает то, что остаётся достижимым: растущий кеш в модульной переменной, не снятые обработчики EventEmitter, таймеры, замыкания над большими объектами.
Вызывают global.gc() через --expose-gc в продакшене, чтобы «почистить память». Это лишь добавляет полные паузы и утечку не лечит.
Считают WeakMap универсальным лекарством. Он помогает, только когда ключом служит объект, жизнью которого вы не управляете, а WeakRef и FinalizationRegistry не гарантируют, когда и будет ли вызван колбэк.