Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Збирач сміття (Garbage Collector)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Збирач сміття це частина рантайму (V8, SpiderMonkey), яка автоматично звільняє пам'ять під об'єктами, недосяжними від коренів програми: глобальних об'єктів, стека викликів, активних замикань, дерева DOM.** Базовий алгоритм це mark-and-sweep: обхід графа від коренів із позначанням живих об'єктів, потім звільнення непозначених, іноді з ущільненням (compaction). Сучасні збирачі поколінські (швидкий minor GC для нових об'єктів, дорожчий major GC для старих), інкрементальні та конкурентні, щоб паузи були короткими. Підрахунок посилань не використовують, бо він ламається на циклах. **Ключове:** GC недетермінований, розробник керує не ним, а посиланнями: знімайте слухачі подій і таймери, обмежуйте кеші, використовуйте `WeakMap`/`WeakSet` і не тримайте зайвих посилань на вузли DOM.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Збирач сміття (garbage collector, GC) це частина рантайму (V8 у Chrome і Node.js, SpiderMonkey у Firefox тощо), яка автоматично звільняє пам'ять, зайняту об'єктами, до яких більше не можна дістатися від коренів (roots) програми.** Головний критерій це досяжність (reachability): якщо об'єкт досяжний від коренів (глобальні об'єкти, стек поточних викликів, регістри, активні замикання, дерево DOM тощо), він живий; якщо недосяжний, його пам'ять може бути звільнена. ## Теорія ### TL;DR - GC видаляє **недосяжні** об'єкти, а не «непотрібні»: єдиний критерій це досяжність від коренів. - Базовий алгоритм це **mark-and-sweep**, часто з **compaction** (ущільненням) проти фрагментації. - Сучасні реалізації **поколінські** (young і old), **інкрементальні** та **конкурентні**, щоб зменшити паузи. - **Підрахунок посилань** (reference counting) у JS не застосовують: він ламається на циклах `A -> B -> A`. - `WeakMap` / `WeakSet` не заважають збиранню, `FinalizationRegistry` дає колбек після збирання, але без гарантій часу. - Для розробника ключове це **керування посиланнями**: слухачі, таймери, кеші, замикання, вузли DOM. ### Швидкий приклад ```javascript let user = { name: 'Alice' }; // об'єкт досяжний від кореня через змінну user let admin = user; // друге посилання на той самий об'єкт user = null; // перше посилання прибрали, але об'єкт ще живий: // його тримає admin, тож GC його не звільнить admin = null; // тепер об'єкт недосяжний і може бути зібраний // (коли саме, вирішує рантайм, а не ваш код) ``` ### Як GC вирішує, що видаляти #### Досяжність (reachability) Корені це те, до чого рантайм має доступ завжди: глобальний об'єкт, змінні у стеку поточних викликів, регістри, активні замикання, дерево DOM. Усе, до чого можна дійти від коренів ланцюжком посилань, вважається живим. Усе інше це сміття. #### Mark-and-sweep 1. **Mark (позначання):** обходимо граф об'єктів від коренів і позначаємо всі досяжні. 2. **Sweep (замітання):** проходимо купою і **звільняємо** всі **непозначені**. Додатково може виконуватися **compaction (ущільнення)**: переміщення вцілілих об'єктів, щоб усунути фрагментацію пам'яті. #### Чому не reference counting Лічильники посилань ламаються на **циклах** (`A -> B -> A`): два об'єкти тримають одне одного, лічильники ніколи не падають до нуля, і виникає витік. Тому в JS використовують **трасувальні** GC (mark-and-sweep або mark-compact), для яких цикл не проблема: якщо компонент недосяжний цілком, його буде зібрано. ### Поколінський, інкрементальний і конкурентний GC #### Поколінський 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** або **mark-compact** для old. #### Write barrier і remembered set Щоб молоде і старе покоління коректно посилались одне на одного, рантайм використовує **write barrier** разом із **card marking / remembered set**: недорогий запис метаданих під час створення посилань «зі старого в молоде», щоб мінорний GC знав, які старі об'єкти можуть тримати молоді. #### Трикольорове маркування і короткі паузи Повна «зупинка світу» (stop-the-world) з великими паузами неприйнятна ані в UI, ані на сервері, тому застосовують: - **Tri-color marking (білий, сірий, чорний):** формальна модель позначання, яка дозволяє **інкрементально** просувати обхід графа. - **Incremental GC:** позначання розбивають на маленькі порції, між ними застосунок продовжує працювати. - **Concurrent GC:** частину роботи (наприклад, розмітку) виконують **паралельно** із застосунком в іншому потоці. - **Паралельні marking і sweeping**, **compaction** з мінімізацією пауз. Ідея проста: зменшувати jank і фризи коштом маленьких передбачуваних пауз. ### Як це влаштовано у V8 #### Простори купи Купа поділена на простори: **new space** (напівпростори копіювального GC), **old space** (mark-sweep або mark-compact), **code space**, **map space**, **large object space** (без переміщення об'єктів). #### Minor GC і Major GC - **Minor GC (Scavenger):** швидко копіює живі об'єкти з from-space до to-space, ті, що пережили N збирань, отримують **promotion** у old space. - **Major GC:** точна (precise) розмітка, потім sweep або compact, інкрементальна і конкурентна, щоб скорочувати паузи. - Тонкощі: write barriers, remembered sets, паралельні marking і sweeping, евристики щодо тиску на пам'ять і фрагментації. #### Коли GC запускається - Немає вільного місця для нового виділення (allocation failure). - Тиск на пам'ять з боку ОС або хоста. - Евристики за зростанням купи і частотою мінорних збирань. - У DevTools збирання можна запустити вручну, але лише для налагодження. ### Слабкі посилання і фіналізація - **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; } ``` ### Витоки пам'яті: практика і діагностика #### Типові джерела витоків 1. **Довгоживучі посилання:** кеші, сінглтони, замикання, глобальні структури, збережені вузли DOM. 2. **Події та таймери:** не зняли обробник або `setInterval`, підвислі посилання з `Promise`. 3. **Цикли DOM і JS:** тримаєте посилання на видалений з DOM вузол або на його замикання. 4. **Необмежені структури:** `Map` або масив ростуть без очищення. #### Що робити - Знімати **обробники подій** і **таймери**: ```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 недетермінований:** ви не контролюєте точний час збирання. - **Фіналізатори** не гарантовано спрацюють до виходу процесу або закриття вкладки. - **Великі паузи** все ще можливі, хоча сучасні збирачі намагаються робити їх короткими. ### Типові помилки - **Думати, що GC видаляє «непотрібні» об'єкти.** Критерій один: недосяжність. Об'єкт, на який є хоч одне живе посилання з кореня, не буде зібрано, навіть якщо він вам більше не потрібен. - **Вірити, що цикл посилань спричиняє витік.** Це проблема підрахунку посилань, а не трасувальних GC: недосяжний цикл збирається без проблем. - **Покладатися на `FinalizationRegistry` для звільнення ресурсів.** Колбек може не виконатися ніколи, закривайте файли, сокети та підписки явно. - **Вважати `delete obj.prop` або `x = null` командою збирачу.** Це лише прибирає одне посилання, збирання відбудеться тоді, коли вирішить рантайм. - **Не знімати слухачі та `setInterval` під час знищення компонента.** Обробник тримає і вузол DOM, і все його замикання, це найчастіший витік у SPA. - **Використовувати звичайний `Map` як кеш за об'єктами.** Він тримає ключі сильно, тому об'єкти житимуть, доки живе кеш, для такого сценарію потрібен `WeakMap`. - **Ганятися за мікрооптимізаціями замість вимірювань.** Спочатку heap snapshot і порівняння retainers, і лише потім зміни в коді.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.