Введение

Когда мы что-то присваиваем переменной, это может быть либо примитивным (значение), либо объектным (ссылочный) типом. В этой статье представлен обзор типов, разница между сильными и слабыми ссылками, а также способы отслеживания сборки мусора (GC).

Примитивные и ссылочные типы

Типы ссылок и значений

Типы значений и ссылок различаются. Наиболее заметные различия: сравнение, хранение и то, как механизмы управляют этими данными.

📝 Сравнение. Примитивные типы сравниваются по его значению, ссылочные типы сравниваются по ссылке:

1 === 1 // true
{} === {} // false

📝 Хранилище: типы значений хранятся в стеке, ссылочные типы хранятся в куче. Само значение — это просто указатель на место в куче, где оно хранится.

// obj is a reference to the `{}`
const obj = {};
 
// a stores the number 123 in stack
const a = 123;

📝 Когда переменная хранится в стеке, она очищается, как только заканчивается контекст выполнения:

// Когда мы вызываем foo, мы помещаем контекст функции в стек вызовов
foo();
 
function foo() {
  // Добавляем в стек fooVariable и так же foo2
  let fooVariable = 123;
  let foo2;
 
  function bar() {
    // Поместите barVariable в стек
    let barVariable = 1234;
 
    // Здесь мы копируем значение
    foo2 = barVariable;
 
    //  Мы выходим из функции `bar`, что означает, что `barVariable` будет удалена
  }
  // Поместите контекст функции `bar` в стек вызовов
  bar();
 
  // Мы выходим из `foo` fn, поэтому `fooVariable` и `foo2` будут удалены
}

Примечание: в этом примере у нас есть 2 объявления типов объектов, можете ли вы их найти?

Отвечать Мы объявили 2 функции: barи foo. Это типы объектов

📝 Поскольку ссылочные типы хранятся в куче, движки должны «понимать», когда они могут собирать объект и очищать неиспользуемые данные, иначе вы получите утечку памяти. Для этого двигатели используют сборщик мусора (#GC ).

Самая простая реализация сборщика мусора — это счетчик ссылок: как видите, мы не очищаем объект, как только выходим из функции, по сравнению с типами значений. Он хранится все время, пока у нас есть какие-либо указатели на него.
Иллюстрация простейшего GC

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

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

// Сильная ссылка на массив
const array = []; 
 
// Сильная ссылка на объект
const obj = {};
 
// Сильная ссылка на карту
const map = new Map();

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

Слабые рефери

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

#WeakRef относительно новичок в экосистеме JS. Они не предотвращают GCed сохраненного значения:
Слабый опорный GC

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

Некоторые примеры, которые я видел раньше:

  1. Если вы хотите контролировать порядок выполнения промисов. Один из самых популярных способов сделать это — выполнить их в порядке объявления: например, «stack promises» . Чтобы предотвратить возможные утечки памяти, вы можете создать список WeakRef<Promise>, и если какое-либо из обещаний будет подвергнуто сборке мусора, вы просто проигнорируете запись в своем списке.

  2. Если у вас есть карта, какие ключи являются объектами, и вы не хотите, чтобы эта карта хранила объекты в памяти. Например, такие карты можно использовать для хранения дополнительных данных, связанных с самим объектом, таких как метаданные. В этом случае вы можете использовать WeakMap

  3. Временные кеши: у нас может быть кеш, который имеет ограничение на максимальное количество записей внутри кеша (например, LRU). Другой вариант использования — создать кеш, в котором хранится список WeakRefs. Такой кеш будет хранить данные до тех пор, пока у нас есть ссылка на них или до того момента, когда сборщик мусора удалит неиспользуемые записи.

JavaScript позволяет нам использовать 3 типа Weak*структур:
📝 1. WeakRefэто объект, который содержит слабую ссылку на другой объект, не предотвращая его GCed.
📝 2. WeakMapявляется картой, но ключи являются слабыми ссылками. Примитивные типы в качестве ключей запрещены в WeakMaps. Он охватывает второй вариант использования из приведенных выше примеров (дополнительные данные к существующим объектам, например метаданные).
📝 3. WeakSetэто набор WeakRefsобъектов.

Важное примечание : не выполняйте тесты с WeakRefs в инструментах разработки. Ваши объекты не будут GCed в нем из-за специфического поведения DevTools. Вместо этого используйте jsbin , codeandbox , runkit или выполняйте тесты в Node.js. Localhost нормально работает, когда вы открываете файлы по http://протоколу, так как file://имеет специфическое поведение.

Давайте проверим некоторое базовое WeakRefповедение. Как мы можем узнать, что объект прошел GCed? Для проведения теста нам понадобится:

  1. Потерять/удалить все сильные ссылки на объект
  2. Выделите много памяти и удалите сильные ссылки на эти объекты.
  3. Делайте макрозадачи паузами, чтобы увеличить шансы на сбор мусора
  4. Тест WeakRef.

Вот так мы можем выделять много памяти и делать паузы

Теперь давайте подготовим тестовый код:

Если бы Runkit не показал пример

Как видите, при выполнении GC мы теряем доступ к объекту с WeakRef. Тем не менее, у нас все еще может быть 2 вопроса:

Почему у нас есть отдельные типы данных для WeakMaps и WeakSet?

Давайте создадим карту с объектами в качестве ключей:

Когда вы сохраняете weakRef в качестве ключа, вы ссылаетесь на сам объект WeakRef вместо реального объекта ( b = {}). Это означает, что для сохранения слабого рефа в качестве ключа на карте нужно использовать специальный тип:

Если бы Runkit не показал пример

📝 Обычная карта не позволяет вам использовать WeakRefs в качестве ключей, вы можете либо ссылаться на сам объект WeakRef, либо иметь сильную ссылку в качестве ключа.
📝 WeakMap создает WeakRef для объектов, которые мы используем в качестве ключей, и не предотвращает их GCed.
📝 WeakMap не имеет методов .entriesили .keysкак обычная карта, поэтому мы не можем перебирать элементы WeakMap. WeakMap имеет следующие методы: .has.get.set.

Как получить точный момент, когда объект получает GCed?

JavaScript обеспечивает #FinalizationRegistry .

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

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

Если бы Runkit не показал пример

📝 Одним из самых популярных вариантов использования FinalizationRegistryявляется логирование памяти. Если ваше приложение работает с большими фрагментами данных, вы можете использовать его FinalizationRegistryдля создания отчетов, чтобы понять, есть ли в вашем приложении потенциальные утечки памяти.

Что с поддержкой браузера?

Вы можете использовать эти инструменты почти во всех современных браузерах: https://caniuse.com/?search=Weak

Слабая ссылка
Слабая карта
Слабый набор
Завершение реестра

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