Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке збирач сміття в JavaScript?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Збирач сміття (garbage collector)** автоматично знаходить і звільняє пам'ять об'єктів, до яких більше немає доступу від коренів (глобальна область, стек викликів). ```javascript let user = { name: "Alice" }; user = null; // недосяжний - кандидат на збір при наступному циклі ``` V8 використовує mark-and-sweep: обходить усе досяжне від коренів, решту звільняє. Циклічні посилання не є проблемою. Реальні витоки - забуті таймери, неприбрані обробники подій, глобальні змінні що ростуть. **Ключове:** присвоєння `null` робить об'єкт кандидатом. GC запускається за власним розкладом, не на вимогу.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Збирач сміття (garbage collector)** - це вбудована частина рушія JavaScript, яка автоматично знаходить і звільняє пам'ять об'єктів, до яких більше немає доступу з активного коду. ## Теорія ### TL;DR - Уяви готельну покоївку: вона забирає залишений багаж тільки після того, як переконалась, що жоден гість не тримає квитанцію на нього. - Немає посилань від коренів (глобальні змінні, стек викликів) = об'єкт є сміттям. - V8 використовує mark-and-sweep: обходить все досяжне від коренів, решту - звільняє. - Циклічні посилання не спричиняють витоків у сучасних рушіях. Mark-and-sweep їх виявляє. - Примусово запустити GC у продакшені не можна. Реальні витоки - забуті таймери, відв'язані DOM-вузли та глобальні змінні, що ростуть. ### Швидкий приклад ```javascript let user = { name: "Alice" }; // пам'ять виділена в heap console.log(user.name); // "Alice" - об'єкт досяжний user = null; // останнє посилання обрізане // { name: "Alice" } більше не досяжний від жодного кореня // V8 звільнить його на наступному циклі збору console.log(typeof user); // "object" - null залишається значенням ``` Після `user = null` жодна частина програми не може дістатись до того об'єкта. Він стає кандидатом на збір. Коли саме рушій звільнить пам'ять - вирішує він сам. ### Як працює mark-and-sweep V8 стартує від набору **коренів**: глобальний об'єкт, поточний стек викликів, активні хендли. Від кожного кореня він обходить усі посилання - властивості, [замикання](/questions/what-is-closure-in-javascript), ланцюжки прототипів - і позначає кожен досяжний об'єкт. Після завершення обходу все непозначене звільняється. Головна ідея: досяжність, а не підрахунок посилань. Цикл між двома об'єктами нічого не означає, якщо жоден з них не досяжний від кореня. Обидва будуть зібрані. ```javascript function createCycle() { let a = {}; let b = {}; a.ref = b; b.ref = a; // цикл створено } createCycle(); // a і b виходять за межі видимості // жоден не досяжний від жодного кореня // V8 збирає обидва - витоку немає ``` Старі рушії з підрахунком посилань мали проблеми саме з таким патерном. Mark-and-sweep - ні. ### Покоління і поступове маркування V8 ділить heap на дві зони. **Молоде покоління (young generation)** містить нещодавно виділені об'єкти, більшість з яких швидко стають сміттям. V8 очищає цю зону швидкими scavenge-проходами. Об'єкти, що пережили кілька раундів, переходять до **старого покоління (old generation)**, де повний mark-and-sweep запускається рідше. З версії V8 7.4 маркування відбувається поступово. Рушій зупиняє головний потік короткими відрізками замість одного довгого блокування. Це зменшує підтикання в UI-застосунках. Node 22+ постачається з GC Orinoco, який покращує пропускну здатність для серверних навантажень. З JS-коду все це непомітно. Але саме тому DevTools іноді показує пам'ять, яка виглядає вільною, але ще не зібрана. ### Причини витоків пам'яті `setInterval` - найпідступніший витік у продакшені. Якось я дебажив Node.js-сервіс, що ріс на 50 МБ на годину, і причиною виявився інтервал всередині обробника запитів, який ніхто не прибирав. GC збирає тільки те, до чого немає доступу, тому якщо посилання залишається живим - об'єкт залишається в пам'яті назавжди. Чотири найпоширеніші причини: - **Таймери**: `setInterval` тримає свій колбек і всі замикання, на які він посилається, живими до виклику `clearInterval`. - **Обробники подій**: слухач на DOM-елементі тримає посилання на свій колбек. Видаляєш елемент без видалення слухача - обидва залишаються в пам'яті. - **Глобальні змінні**: `window.cache = []` є коренем. Все, що туди потрапляє, ніколи не збирається. Для кешів, ключами яких є об'єкти, використовуй [WeakMap](/questions/what-is-weakmap-in-javascript). - **Відв'язані DOM-вузли**: видалення елемента з дерева DOM не звільняє його, якщо JS-змінна досі тримає посилання. ### Типові помилки **Помилка: думати що `delete` звільняє пам'ять** ```javascript let obj = { prop: "value" }; delete obj.prop; // видаляє ключ з об'єкта // сам obj досі виділений в heap - розмір пам'яті не змінився ``` `delete` видаляє ключ з об'єкта. Щоб зробити об'єкт кандидатом на збір, треба обнулити посилання: `obj = null`. **Помилка: зберігати необмежений стан у глобальних змінних** ```javascript window.cache = []; function add() { window.cache.push(new Array(1_000_000)); } // cache є коренем, росте вічно ``` Тримай дані всередині функції або модуля. Для кешів, ключами яких є об'єкти, використовуй `WeakMap`, щоб записи звільнялись автоматично. **Помилка: не прибирати обробники подій** ```javascript element.addEventListener("click", handler); // елемент видалений з DOM, але замикання handler досі живе ``` Викликай `removeEventListener` в коді очищення або використовуй `AbortController` для скасування кількох слухачів одразу. **Помилка: очікувати миттєвого збору** ```javascript largeObj = null; // пам'ять може ще секунди показуватись як зайнята в DevTools ``` Присвоєння `null` робить об'єкт кандидатом. GC запускається за власним розкладом і не реагує на твій код. Відстежуй тренди в heap snapshot, а не окремі виміри. ### Реальне використання - React: функції очищення в `useEffect` (`clearInterval`, `removeEventListener`) запобігають витокам замикань у дашбордах і SPA. - Node.js/Express: `WeakMap` для кешів на рівні запиту дозволяє записам збиратись, коли об'єкт запиту виходить за межі видимості. - Lodash: використовує `WeakMap` всередині `memoize`, щоб кешовані результати звільнялись разом з вихідним об'єктом. - Chrome DevTools, вкладка Memory: snapshot до і після дії дозволяє порівняти Retained Size і знайти, що тримає пам'ять. ### Питання на співбесіді **Q:** Як V8 виявляє, що об'єкт недосяжний? **A:** Стартує від коренів (глобальний об'єкт, стек) і обходить усі посилання. Об'єкти, до яких обхід не дійшов, є недосяжними. Це фаза маркування в mark-and-sweep. **Q:** Чи спричиняють циклічні посилання витоки в сучасному JS? **A:** Ні. Mark-and-sweep перевіряє досяжність від коренів, а не кількість посилань. Проблема існувала в старих рушіях з підрахунком посилань, але не у V8. **Q:** Як примусово запустити GC під час розробки? **A:** Запусти Node.js з `--expose-gc` і викликай `global.gc()`. У браузерах такого способу немає в продакшені. Chrome DevTools дозволяє вручну запустити збір у вкладці Memory. **Q:** У чому різниця між молодим і старим поколіннями? **A:** Молоде покоління тримає короткоживучі об'єкти і очищується швидкими scavenge-проходами. Старе покоління тримає об'єкти, що пережили кілька раундів, і очищується повним mark-and-sweep. Більшість об'єктів ніколи не виходять з молодого покоління, тому типові застосунки залишаються швидкими. **Q:** Сервіс на Node.js виростає з 300 МБ до 2 ГБ за добу. Як дебажити? **A:** Роби heap snapshot з інтервалом і порівнюй колонку Retained Size. Дивись, що росте - скоріш за все масив, Map або EventEmitter, що накопичує записи. Підозрілі кандидати: `setInterval`-колбеки і будь-який глобальний кеш без обмеження розміру. Додай `process.memoryUsage()` до ендпоінту метрик, щоб підтвердити тренд до відкриття DevTools. ## Приклади ### Базовий: обрізання посилання ```javascript function allocate() { let data = new Array(100_000).fill("x"); // велике виділення в heap return data; } let result = allocate(); // result тримає масив result = null; // масив більше не досяжний, кандидат на збір ``` Після `result = null` зникає останнє посилання. Масив зі 100 тисяч елементів стає сміттям на наступному проході збору. ### Середній: очищення в React запобігає витоку ```javascript function Dashboard() { const [items, setItems] = useState([]); useEffect(() => { const id = setInterval(() => { setItems(prev => [...prev, { ts: Date.now() }]); }, 1000); return () => clearInterval(id); // очищення при розмонтуванні }, []); return <div>{items.length} items loaded</div>; } ``` Без `clearInterval` інтервал продовжує працювати після розмонтування `Dashboard`. Кожен тік додає запис до `items`, замикання тримає стан компонента живим, heap росте до перезавантаження сторінки. Функція очищення - це те, що дозволяє GC виконати свою роботу. ### Просунутий: WeakMap для метаданих без витоку ```javascript const metadata = new WeakMap(); function attachMeta(obj, info) { metadata.set(obj, info); } let request = { id: 42 }; attachMeta(request, { startTime: Date.now() }); request = null; // WeakMap не заважає збору сміття // коли request збирається, його запис у metadata зникає автоматично // звичайна Map тримала б посилання і спричинила витік ``` Ключі `WeakMap` утримуються слабко. Коли `request` стає `null` і інших посилань немає - об'єкт збирається, а відповідний запис у `WeakMap` зникає разом з ним. Звичайна `Map` тримала б об'єкт живим нескінченно - це саме той патерн витоку, від якого [WeakMap](/questions/what-is-weakmap-in-javascript) і захищає.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.