Когда вы создаете переменные или объекты в JavaScript, браузер выделяет память для хранения их данных. 

Но если не освобождать эту память, когда данные больше не нужны, это может привести к тому, что приложение со временем будет потреблять все больше и больше памяти. В конечном итоге такая перегрузка вызовет замедление работы или аварийное завершение работы приложения.

Проблема решается с помощью сборки мусора, которая автоматически очищает неиспользуемую память.

В JavaScript объект считается «достижимым», если к нему все еще можно получить прямой или косвенный доступ через код. Объекты, которые больше недоступны, будут помечены для сбора в мусор.

let user = { name: "Alice" }; // Для этого объекта выделяется память.

user = null; // Теперь объект недоступен и будет очищен сборщиком мусора.

Движки JavaScript (например, V8 в Chrome и Node.js) используют алгоритмы, чтобы определить, какие объекты больше не доступны и могут быть безопасно удалены из памяти.

В JavaScript ссылки на объекты по умолчанию являются сильными.

Сильная ссылка означает следующее: пока существует ссылка на объект, он не будет собран в мусор.

let strongRef = { name: "Strong Reference" };

console.log(strongRef.name); // Вывод: "Strong Reference" ("Сильная ссылка")

// Объект, на который ссылается `strongRef`, не будет собираться в мусор до тех пор, пока существует ссылка.

Пока переменная strongRef хранит ссылку на объект, он остается в памяти.

Слабые ссылки, представленные посредством объекта WeakRef, позволяют ссылаться на объект, не препятствуя его сборке в мусор.

let obj = { name: "Weak Reference" };
let weakRef = new WeakRef(obj);

// Обращение к слабой ссылке
console.log(weakRef.deref()?.name); // Вывод: "Weak Reference" ("Слабая ссылка")

// Теперь удалите сильную ссылку
obj = null;

// Теперь объект может быть собран в мусор, поскольку существует только слабая ссылка.
// Обращение к слабой ссылке после сборки мусора может вернуть "undefined"
setTimeout(() => {
console.log(weakRef.deref()); // Вероятный вывод: undefined (если мусор собран)
}, 1000);

weakRef.deref() используется для доступа к объекту, на который ведет слабая ссылка, если он еще существует. Если объект был собран в мусор, deref() возвращает undefined.

Слабые ссылки могут быть полезны при использовании механизмов кэширования или других сценариев, когда нужно, чтобы объекты собирались в мусор, если на них больше не ведут сильные ссылки в других местах.

Распространенные алгоритмы сбора мусора

В процессе написания программы вы создаете переменные и объекты для хранения данных. Однако по мере выполнения программы некоторые из этих объектов могут больше не понадобиться. Сборка мусора — это процесс автоматической очистки неиспользуемых объектов с целью освобождения памяти.

Представьте, что ваша программа — это стол. Каждый создаваемый объект — это стикер, который вы наклеиваете на этот стол. Когда стикер вам больше не нужен, вы можете его убрать. Сборка мусора — своеобразный помощник, который приходит и убирает стикеры, которые вы больше не используете.

Теперь, когда вы знаете основы, рассмотрим, как сборка мусора реализуется с помощью различных алгоритмов.

1. Подсчет ссылок

  • Каждый объект ведет подсчет того, сколько «стикеров» (ссылок) указывают на него.
  • Если объект не используется (счетчик ссылок становится равным 0), его можно смело выбрасывать (собирать в мусор).
function example() {
let objA = { name: "Object A" }; // Количество ссылок на objA равно 1.
let objB = objA; // objB также указывает на objA. Теперь количество ссылок равно 2.

objA = null; // Одна ссылка удалена. Количество ссылок равно 1.
objB = null; // Последняя ссылка удалена. Количество ссылок равно 0.
// objA подходит для сбора в мусор.
}

Процесс подсчета ссылок может дать сбой, когда два или более объекта ссылаются друг на друга, создавая «замкнутый круг».

Даже если вы больше не используете эти объекты, количество указывающих на них ссылок не уменьшится до 0.

function circularRef() {
let objA = {};
let objB = {};
objA.ref = objB; // objA ссылается на objB.
objB.ref = objA; // objB ссылается на objA.

// Даже если вы удалите все внешние ссылки на objA и objB,
// они все равно будут ссылаться друг на друга, поэтому количество их ссылок никогда не снизится до 0.
}

Поэтому современные алгоритмы сборки мусора не полагаются исключительно на подсчет ссылок.

2. Алгоритм Mark and Sweep

  • Сборщик мусора начинает с «корневых» объектов, которые всегда доступны (например, объект window в браузере или глобальные переменные).
  • Он проходит по всем ссылкам на корневые объекты и помечает все, до чего может добраться, как «активные объекты» (все еще необходимые).
  • Объекты, которые не помечены, считаются недостижимыми (больше не нужны) и удаляются из памяти.
let obj1 = { key: "value" };
let obj2 = { otherKey: "otherValue" };

obj1.ref = obj2; // obj1 ссылается на obj2.
obj2 = null; // obj2 по-прежнему доступен, потому что на него ссылается obj1.

obj1 = null; // Теперь ни obj1, ни obj2 недоступны.

Когда obj1 присваивается null, сборщик мусора удостоверяется в следующем:

  •  obj1 больше не доступен.
  • obj2 также недостижим, потому что его на него ссылался только obj1.

И obj1, и obj2 будут убраны в мусор.

Сборка мусора по поколениям

В современных движках JavaScript, таких как V8 (используется в Chrome и Node.js), память делится на две основные области:

Молодое поколение

  • Здесь размещаются недолговечные объекты (например, временные переменные, вызовы функций).
  • Сборка мусора в этой области происходит часто, потому что большинство объектов создаются тут и быстро становятся неиспользуемыми.

Старое поколение

  • Здесь хранятся долгоживущие объекты (например, глобальные переменные, объекты, хранящиеся в памяти в течение длительного времени).
  • Сборка мусора тут происходит реже, так как ожидается, что долгоживущие объекты будут использоваться дольше.

Когда вы создаете новые объекты в коде, они первоначально размещаются в молодом поколении. Сборщик мусора часто проверяет молодое поколение.

Объекты, которые больше недоступны, удаляются.

Объекты, которые все еще используются, переходят в старое поколение, пережив несколько циклов сборки мусора. Сборка мусора здесь происходит реже, поскольку эта область содержит долгоживущие объекты.

Полная сборка мусора занимает больше времени, так как сборщик мусора должен обойти весь граф памяти.

Инкрементальная сборка мусора

Вместо того чтобы останавливать всю программу для выполнения сборки мусора сразу (что может привести к заметным задержкам или «паузам»), инкрементальная сборка мусора разбивает процесс удаления мусора на более мелкие шаги.

Эти шаги выполняются параллельно с выполнением программы, что снижает влияние на производительность и обеспечивает более плавную работу приложения.

let objects = [];

function createObjects() {
for (let i = 0; i < 1000; i++) {
objects.push({ id: i, value: `Object ${i}` });
}
console.log("Objects created");
}

function clearObjects() {
// Удаление ссылок на объекты
objects = [];
console.log("Objects cleared");
}

// Моделирование взаимодействия пользователя с приложением
setInterval(() => {
createObjects(); // Создает временные объекты
clearObjects(); // Очищает объекты, чтобы сделать их пригодными для сбора в мусор
}, 1000);

Вместо того чтобы полностью приостанавливать выполнение программы для сбора недоступных объектов, сборщик мусора:

  • Помечает часть недоступных объектов как мусор.
  • Убирает их в мусор небольшими порциями.

Это позволяет программе (например, setInterval) продолжать работать без сбоев.

В таких приложениях, как игры, или веб-приложениях с насыщенным пользовательским интерфейсом, инкрементальная сборка мусора обеспечивает плавное взаимодействие, предотвращая внезапные «зависания».

Слабые ссылки

Слабая ссылка — это особый вид ссылки на объект, который не препятствует его сборке в мусор.

Это означает, что даже если на объект по-прежнему ссылается WeakMap или WeakSet, его можно удалить из памяти, если на него нет других сильных ссылок.

Слабые ссылки особенно полезны в таких ситуациях, как кэширование или управление ресурсами, когда не нужно, чтобы ссылка препятствовала очистке объекта, когда он больше не нужен.

Принцип работы WeakMap

Представьте, что вы храните список людей (объектов). Если вы сохраните ссылки на них в обычном Map или Set, они останутся в памяти навсегда, пока вы не удалите их вручную — даже если они «покинули здание» (стали недоступны).

WeakMap хранит пары «ключ-значение», где ключами являются только объекты (не примитивы).

Если ключ-объект становится недоступным, он попадает в сборку мусора, и связанное с ним значение также удаляется автоматически.

let cache = new WeakMap();

function loadData(key) {
if (cache.has(key)) {
return cache.get(key); // Возвращение кэшированных данных
} else {
let data = { value: `Data for ${key.name}` }; // Создание новых данных
cache.set(key, data); // Кэширование данных
return data;
}
}

let obj1 = { name: "Object 1" };
let obj2 = { name: "Object 2" };

console.log(loadData(obj1)); // Загружает и кэширует данные для obj1
console.log(loadData(obj2)); // Загружает и кэширует данные для obj2

obj1 = null; // Удаление сильной ссылки на obj1
// Данные obj1 в WeakMap теперь могут быть собраны в мусор.
  • Когда obj1 устанавливается в null, сильных ссылок не остается.
  • Объект и его кэшированные данные в WeakMap автоматически удаляются сборщиком мусора.

Принцип работы WeakSet

А теперь представьте себе тот же список, но со стикерами, которые автоматически исчезают, когда человек «покидает здание». Таким образом, вам не придется беспокоиться об уборке!

WeakSet хранит объекты и следит за тем, чтобы не было дубликатов.

Если объект в WeakSet становится недоступным, он автоматически удаляется.

let weakSet = new WeakSet();

let obj1 = { name: "Object 1" };
let obj2 = { name: "Object 2" };

weakSet.add(obj1); // Добавьте obj1 в WeakSet
weakSet.add(obj2); // Добавьте obj2 в WeakSet

console.log(weakSet.has(obj1)); // true
console.log(weakSet.has(obj2)); // true

obj1 = null; // Удалите сильную ссылку на obj1
// obj1 теперь может быть использован для сбора мусора.

Когда obj1 становится недоступным, сборщик мусора удаляет его из WeakSet.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Rajeswari Depala: Garbage Collection in Javascript

Предыдущая статьяКак решить реальную задачу при помощи структурированной конкурентности и виртуальных потоков Java 21
Следующая статьяC++: полное руководство по разделению строк