Skip to main content

Що таке збирач сміття (Garbage Collector)? Розкажіть детально, як він влаштований

Що таке збирач сміття (GC)

Збирач сміття - це частина рантайму (V8 у Chrome/Node, SpiderMonkey у Firefox тощо), яка автоматично звільняє пам'ять, зайняту об'єктами, до яких більше не можна дістатися з коренів (roots) програми.

Головний критерій: досяжність (reachability). Якщо об'єкт досяжний з коренів (глобальні об'єкти, стек поточних викликів, регістри, активні замикання, DOM-дерево тощо), він живий. Якщо не досяжний, його пам'ять може бути звільнена.


Базовий алгоритм: mark-and-sweep

  1. Mark (позначення): обходимо граф об'єктів від коренів, позначаємо всі досяжні.
  2. 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: дозволяє виконати колбек після того, як об'єкт зібрано. Важливі попередження: час виклику не детермінований, порядок не гарантований, виклику може не бути (при завершенні процесу). Не можна покладатися на це для логіки; тільки для "м'яких" побічних дій (очищення зовнішніх кешів тощо).

Приклад безпечного кеша:

javascript
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-застосунках:

  1. Довгоживучі посилання: кеші, синглтони, замикання, глобальні структури, збережені DOM-вузли.
  2. Події/таймери: не зняли обробник або setInterval; висячі Promise-посилання.
  3. DOM<->JS цикли: тримаєте посилання на видалений з DOM вузол або його замикання.
  4. Необмежені структури: Map/Array ростуть без очищення.

Що робити:

  • Відключати обробники подій і таймери:

    javascript
    const 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) і діагностувати поведінку пам'яті інструментами профілювання.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.