Введение
Когда мы что-то присваиваем переменной, это может быть либо примитивным (значение), либо объектным (ссылочный) типом. В этой статье представлен обзор типов, разница между сильными и слабыми ссылками, а также способы отслеживания сборки мусора (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 ).
Самая простая реализация сборщика мусора — это счетчик ссылок: как видите, мы не очищаем объект, как только выходим из функции, по сравнению с типами значений. Он хранится все время, пока у нас есть какие-либо указатели на него.
Современные движки используют сложные сборщики мусора, которые имеют множество оптимизаций, таких как поколения и т. д. (дайте мне знать, если вам это интересно, мы можем рассказать об этом в будущем 😊)
Все переменные, которые мы использовали до сих пор, являются сильными ссылками. Мы используем их чаще всего:
// Сильная ссылка на массив
const array = [];
// Сильная ссылка на объект
const obj = {};
// Сильная ссылка на карту
const map = new Map();
В отличие от сильных ссылок, многие языки позволяют разработчикам использовать слабые ссылки.
Слабые рефери
📝 Слабые ссылки — это ссылки на объекты, которые удаляются сборщиком мусора. Другими словами, они не предотвращают сбор мусора для объекта.
#WeakRef относительно новичок в экосистеме JS. Они не предотвращают GCed сохраненного значения:
📝 Слабые ссылки используются для предотвращения хранения объектов в памяти, когда нет других сильных ссылок, и вы не хотите, чтобы эта ссылка мешала этому.
Некоторые примеры, которые я видел раньше:
-
Если вы хотите контролировать порядок выполнения промисов. Один из самых популярных способов сделать это — выполнить их в порядке объявления: например, «stack promises» . Чтобы предотвратить возможные утечки памяти, вы можете создать список
WeakRef<Promise>
, и если какое-либо из обещаний будет подвергнуто сборке мусора, вы просто проигнорируете запись в своем списке. -
Если у вас есть карта, какие ключи являются объектами, и вы не хотите, чтобы эта карта хранила объекты в памяти. Например, такие карты можно использовать для хранения дополнительных данных, связанных с самим объектом, таких как метаданные. В этом случае вы можете использовать
WeakMap
-
Временные кеши: у нас может быть кеш, который имеет ограничение на максимальное количество записей внутри кеша (например, 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? Для проведения теста нам понадобится:
- Потерять/удалить все сильные ссылки на объект
- Выделите много памяти и удалите сильные ссылки на эти объекты.
- Делайте макрозадачи паузами, чтобы увеличить шансы на сбор мусора
- Тест 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
Сегодня мы обсудили примитивные и объектные типы, следуя за слабыми и сильными ссылками, и поэкспериментировали с тем, как отслеживать момент, когда объект вот-вот будет подвергнут сборке мусора.