Що таке збирач сміття (Garbage Collector)? Розкажіть детально, як він влаштований
Що таке збирач сміття (GC)
Збирач сміття - це частина рантайму (V8 у Chrome/Node, SpiderMonkey у Firefox тощо), яка автоматично звільняє пам'ять, зайняту об'єктами, до яких більше не можна дістатися з коренів (roots) програми.
Головний критерій: досяжність (reachability). Якщо об'єкт досяжний з коренів (глобальні об'єкти, стек поточних викликів, регістри, активні замикання, DOM-дерево тощо), він живий. Якщо не досяжний, його пам'ять може бути звільнена.
Базовий алгоритм: mark-and-sweep
- Mark (позначення): обходимо граф об'єктів від коренів, позначаємо всі досяжні.
- Sweep (змітання): проходимо по купі і звільняємо всі непозначені. Додатково може виконуватися compaction (ущільнення) - переміщення об'єктів, що вціліли, для усунення фрагментації пам'яті.
Поколіннєвий GC (generational)
Спостереження: "більшість об'єктів живуть недовго". Звідси:
- Young/нове покоління (nursery/new space): сюди потрапляють нові об'єкти; часті швидкі збирання (minor GC / scavenging).
- Old/старе покоління (tenured/old space): сюди просувають (promote) ті об'єкти, що пережили декілька мінорних збирань; збираються рідше, але дорожче (major GC).
- Технічно реалізується через:
- bump-pointer allocation (дуже швидке виділення поспіль),
- copying GC для young (два напівпростори: from-space -> to-space),
- mark-(sweep|compact) для old.
Щоб молоде і старе покоління коректно посилалися одне на одного, рантайм використовує:
- write barrier + card marking / remembered set - недороге записування метаданих при створенні посилань "з old у young", щоб мінорний GC знав, які старі об'єкти можуть тримати молоді.
Інкрементальність, конкурентність і три-кольорова позначка
Повна "зупинка світу" (stop-the-world) на великі паузи неприйнятна в UI/сервері, тому застосовують:
- Tri-color marking (білий/сірий/чорний): формальна модель позначення, що дозволяє інкрементально просувати обхід графа.
- Incremental GC: позначення розбивають на маленькі порції, між ними застосунок продовжує працювати.
- Concurrent GC: частина роботи (наприклад, розмітка) виконується паралельно з застосунком в іншому потоці.
- Incremental/parallel marking & sweeping, compaction з мінімізацією пауз.
Ідея: зменшувати jank/фризи за рахунок маленьких передбачуваних пауз.
Чому не "reference counting"
Лічильники посилань ламаються на циклах (A->B->A), що призводить до витоків. Тому в JS - трасувальні GC (mark-and-sweep/compact), де цикл не проблема: якщо компонент недосяжний цілком, він збереться.
Weak-посилання і фіналізація
- WeakMap/WeakSet: ключі (для WeakMap) і елементи (у WeakSet) не перешкоджають збиранню. Щойно об'єкт більше ніде не досяжний, GC може його звільнити, запис зникне "сам". Це корисно для кешів/мемоізації без витоків.
- FinalizationRegistry: дозволяє виконати колбек після того, як об'єкт зібрано. Важливі попередження: час виклику не детермінований, порядок не гарантований, виклику може не бути (при завершенні процесу). Не можна покладатися на це для логіки; тільки для "м'яких" побічних дій (очищення зовнішніх кешів тощо).
Приклад безпечного кеша:
const cache = new WeakMap(); // ключ - об'єкт, значення - обчислення
function heavy(obj) {
if (cache.has(obj)) return cache.get(obj);
const val = compute(obj);
cache.set(obj, val);
return val;
}Що конкретно в V8 (Chrome/Node) - у двох словах
- Купа розділена на простори: new space (напівпростори копіювального GC), old space (mark-sweep/compact), code space, map space, large object space (без переміщення).
- Minor GC (Scavenger): швидко копіює живі об'єкти з from-space в to-space; ті, що пережили N разів - promotion в old.
- Major GC: точна (precise) розмітка, потім sweep/compact; інкрементальна і конкурентна, щоб скорочувати паузи.
- Тонкощі: write barriers, remembered sets, parallel marking/sweeping, евристики за тиском пам'яті і фрагментацією.
Коли GC запускається
- Немає вільного місця для нового виділення (allocation failure).
- Тиск пам'яті від ОС/хоста.
- Евристики за ростом купи і частотою мінорних збирань.
- У DevTools можна тригернути вручну (тільки для відладки).
Практика: як не "підставляти" GC
Типові джерела витоків у JS-застосунках:
- Довгоживучі посилання: кеші, синглтони, замикання, глобальні структури, збережені DOM-вузли.
- Події/таймери: не зняли обробник або
setInterval; висячіPromise-посилання. - DOM<->JS цикли: тримаєте посилання на видалений з DOM вузол або його замикання.
- Необмежені структури: Map/Array ростуть без очищення.
Що робити:
-
Відключати обробники подій і таймери:
javascriptconst handler = () => {}; el.addEventListener('click', handler); // ... el.removeEventListener('click', handler); -
Для кешів, прив'язаних до об'єктів - WeakMap/WeakSet.
-
Уникати непотрібних глобальних/модульних посилань.
-
Обережно з замиканнями: не замикайте зайві великі об'єкти.
-
Обмежувати розмір кешів (LRU) і очищати їх.
-
У React - стежити за cleanup у
useEffect.
Інструменти діагностики
- Chrome DevTools -> Performance/Memory: Heap snapshot, Allocation instrumentation -> шукаємо "Detached HTML nodes", ріст retainers.
performance.memory(браузер, експериментально) - груба телеметрія.- Node.js:
--inspect, heap snapshots,clinic/heapdump, профілювальники, прапори--max-old-space-size. - Ідея тесту: прожени сценарій N разів -> пам'ять повинна стабілізуватися. Постійний ріст - сигнал витоку.
Важливі властивості й обмеження
- GC не детермінований: ти не контролюєш точний час збирання.
- Finalizers не гарантуються до моменту виходу процесу/вкладки.
- Великі паузи все ще можливі, але сучасні GC прагнуть робити їх короткими.
Коротке резюме
GC видаляє недосяжні об'єкти. Сучасні реалізації використовують поколіннєвий, інкрементальний, конкурентний mark-and-(sweep/compact), зменшуючи паузи. Для розробника ключове - керувати посиланнями (слухачі, таймери, кеші, замикання, DOM) і діагностувати поведінку пам'яті інструментами профілювання.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.