← Назад к вопросам

Как Garbage Collector очищает память?

2.3 Middle🔥 171 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работает Garbage Collector (сборщик мусора)

Garbage Collector (GC) — это механизм в JavaScript движке, который автоматически освобождает память от объектов, которые больше не используются. Это ключевой механизм управления памятью в JavaScript.

Основная идея

В любой момент времени объект может быть в двух состояниях:

  1. Достижимый (reachable) — приложение может до него добраться
  2. Недостижимый (unreachable) — на него нет ссылок, приложение не может его использовать

GC удаляет недостижимые объекты.

Как определить достижимость?

Root объекты

Это объекты, которые всегда считаются достижимыми:

  • Глобальные переменные (window, global)
  • Переменные в стеке вызовов
  • Замыкания функций
let globalVar = { name: 'Global' }; // Достижимо из window

function foo() {
  let localVar = { name: 'Local' }; // Достижимо из стека
  return localVar;
}

let obj = foo(); // Всё ещё достижимо, т.к. присвоено переменной
// После выхода из foo() localVar удаляется из стека, но obj всё ещё достижим

Граф достижимости

GC строит граф объектов и их ссылок:

let person = {
  name: 'Alice',
  address: {
    city: 'New York'
  },
  hobbies: ['reading', 'coding']
};

// Граф:
// person (достижимо из переменной)
//   ├── address (достижимо через person.address)
//   │   └── city: 'New York'
//   └── hobbies (достижимо через person.hobbies)
//       ├── 'reading'
//       └── 'coding'

person = null; // person становится недостижимым
// GC удалит person, address, hobbies и их содержимое

Алгоритмы GC

1. Mark-and-Sweep (метка и очистка)

Это наиболее распространённый алгоритм. Работает в 3 этапа:

Этап 1: Mark (маркировка)

GC проходит по графу и помечает все достижимые объекты:

let obj1 = { value: 1 };
let obj2 = { value: 2 };
let obj3 = { value: 3 };

obj1.ref = obj2; // obj1 -> obj2

// Mark-фаза:
// Стартуем с root (глобальные переменные)
// Помечаем obj1 (достижимо)
// Помечаем obj2 (достижимо через obj1.ref)
// obj3 НЕ помечен (недостижим)

Этап 2: Sweep (очистка)

GC проходит по памяти и удаляет неотмеченные объекты:

// После Sweep-фазы:
// obj1 сохранён
// obj2 сохранён
// obj3 удалён, память освобождена

Пример Mark-and-Sweep

// Состояние памяти
let cache = {};

cache.image1 = new Image(); // большой объект, помечен
cache.image2 = new Image(); // большой объект, помечен
cache.image3 = new Image(); // большой объект, помечен

// Удаляем ссылку
delete cache.image3;

// Mark phase: image3 не помечен
// Sweep phase: image3 удалён, память освобождена

2. Generational GC (поколенческая сборка мусора)

Основана на предположении: молодые объекты часто умирают, старые живут долго.

Объекты делятся на поколения:

// Поколение 0 (молодое): быстро удаляется
for (let i = 0; i < 1000; i++) {
  const temp = { /* большие данные */ };
  processData(temp);
  // temp удаляется в конце итерации, GC его быстро подхватит
}

// Поколение 1+ (старое): собирается реже
const cache = new Map();
// cache существует весь жизненный цикл приложения
// GC редко проверяет его

3. Incremental GC (инкрементальная сборка)

Вместо одной большой паузы, GC работает порциями:

// Без incremental GC (вариант, когда GC работает слишком долго):
// Время: [--- главный код ---][---- GC пауза 50ms ----][--- главный код ---]
//                              ^ пользователь видит заморозку

// С incremental GC:
// Время: [код][GC 5ms][код][GC 5ms][код][GC 5ms][код]
//        Плавнее, нет заметных пауз

Проблемы с памятью и утечки

Memory leak (утечка памяти)

Это когда объект становится недостижимым, но на него всё ещё есть ссылка:

Пример 1: Event listener

// ПЛОХО: утечка памяти
class Component {
  constructor() {
    document.addEventListener('click', () => {
      this.handleClick();
    });
    // Слушатель создан, но никогда не удалён
    // Он держит ссылку на this
  }

  handleClick() {
    console.log('Clicked');
  }
}

// Компонент удалён, но слушатель остался
let comp = new Component();
comp = null; // Компонент НЕ удалится, т.к. слушатель ещё на него ссылается

// ХОРОШО: управляем слушателем
class Component {
  constructor() {
    this.handler = () => this.handleClick();
    document.addEventListener('click', this.handler);
  }

  destroy() {
    document.removeEventListener('click', this.handler);
  }
}

let comp = new Component();
comp.destroy(); // Слушатель удалён
comp = null; // Теперь компонент удалится

Пример 2: Замыкание

// ПЛОХО: утечка памяти
function createCache() {
  const largeData = new Array(10000000); // большой массив
  
  return function() {
    console.log(largeData.length); // замыкание держит ссылку на largeData
  };
}

const getLength = createCache();
// largeData не удалится, пока существует getLength
getLength(); // 10000000

// ХОРОШО: освобождаем замыкание
const getLength = createCache();
getLength();
getLength = null; // Теперь largeData может быть собран

Пример 3: Timer

// ПЛОХО: утечка памяти
function startTimer() {
  const bigData = new Array(1000000);
  
  setInterval(() => {
    console.log(bigData.length); // timer держит ссылку на bigData
  }, 1000);
  // Timer не очищен, bigData не удалится
}

// ХОРОШО: управляем timer
function startTimer() {
  const bigData = new Array(1000000);
  
  const timerId = setInterval(() => {
    console.log(bigData.length);
  }, 1000);
  
  return () => clearInterval(timerId); // функция очистки
}

const stop = startTimer();
stop(); // Очищаем timer

Пример 4: React компонент

// ПЛОХО: утечка памяти в React
function UserProfile() {
  useEffect(() => {
    const fetchUser = async () => {
      const user = await fetch(`/api/user`);
      setUser(user);
    };
    
    fetchUser();
    // Нет cleanup функции!
  }, []);
  
  return <div>{user?.name}</div>;
}
// Если компонент размонтируется до завершения fetch, setUser вызовется на удалённом компоненте

// ХОРОШО: cleanup функция
function UserProfile() {
  useEffect(() => {
    let isMounted = true;
    
    const fetchUser = async () => {
      const user = await fetch(`/api/user`);
      if (isMounted) setUser(user); // Проверяем, что компонент ещё mounted
    };
    
    fetchUser();
    
    return () => {
      isMounted = false; // cleanup: отменяем обновления
    };
  }, []);
  
  return <div>{user?.name}</div>;
}

Инструменты для отладки памяти

Chrome DevTools Memory

  1. Откройте DevTools -> Memory tab
  2. Возьмите snapshot памяти
  3. Проведите операции в приложении
  4. Возьмите ещё один snapshot
  5. Сравните — какие объекты не удалились

Performance Monitoring

// Проверь, как меняется используемая память
if (performance.memory) {
  console.log(`Используется: ${performance.memory.usedJSHeapSize / 1048576} MB`);
  console.log(`Лимит: ${performance.memory.jsHeapSizeLimit / 1048576} MB`);
}

Best Practices

  1. Удаляй слушатели событий
element.addEventListener('click', handler);
// ...
element.removeEventListener('click', handler);
  1. Очищай timers
const id = setTimeout(() => { /* ... */ }, 1000);
clearTimeout(id);
  1. Используй cleanup в useEffect
useEffect(() => {
  // ...
  return () => { /* cleanup */ };
}, []);
  1. Обнуляй большие объекты
let bigArray = new Array(1000000);
// когда больше не нужен
bigArray = null; // помогает GC
  1. Избегай циклических ссылок (В современных движках это не проблема, но старайся)
const obj = {};
obj.self = obj; // циклическая ссылка
obj = null; // GC может это обработать

Итог

Garbage Collector — это невидимый помощник, который:

  • Отслеживает достижимость объектов
  • Удаляет недостижимые объекты
  • Освобождает память автоматически

Ваша задача — помочь ему, избегая утечек памяти:

  • Правильно управляйте слушателями
  • Очищайте timers
  • Используйте cleanup функции в React
  • Помните о замыканиях
Как Garbage Collector очищает память? | PrepBro