Начиная с управления памятью в JS - слабые ссылки в JS

JavaScript

Эта статья была впервые опубликована в публичном аккаунте: CoyPan, как и ожидалось.

написать впереди

Во всех языках программирования, когда мы объявляем переменную, нам нужно, чтобы система выделила для нас блок памяти. Когда нам больше не нужна эта переменная, память нужно высвободить (этот процесс называется сборкой мусора). В языке C есть malloc и free, которые помогают нам в управлении памятью. В JS разработчикам не нужно вручную управлять памятью, движок JS сделает это за нас автоматически. Однако это не означает, что нам не нужно беспокоиться о проблемах с памятью при написании кода на JS.

Выделение памяти и переменные в JS

Цикл объявления памяти выглядит следующим образом:

  1. Выделите необходимую память
  2. Использовать выделенную память (чтение, запись)
  3. отпустите его, когда он не нужен

В JS эти три шага нечувствительны к разработчикам и не требуют от нас слишком много внимания.

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

Основные типы: строка, число, логическое значение, нуль, неопределенный, символ

Типы ссылок: объект, массив, функция

Для переменных базового типа система выделяет для них часть памяти, и в этой части памяти хранится содержимое переменной.

Для переменных ссылочного типа он хранит только адрес, а блок памяти, на который указывает этот адрес, является реальным содержимым переменной.. Присвоение ссылочной переменной также просто передает (копирует) адрес. Например:

// a 和 b 指向同一块内存
var a = [1,2,3];
var b = a;
a.push(4);
console.log(b); // [1,2,3,4]

Еще один момент, который следует отметить, это то, что параметры функции в JS фактически передаются по значению (передаются по ссылке). Например:

// 函数f的入参,其实是把 a 的值复制了一份。注意 a 是一个引用类型变量,其保存的是一个指向内存块的一个地址。
function f(obj) {
	obj.b = 1;
}
var a = { a : 1};
f(a);
console.log(a); // { a: 1, b: 1}

В обычной разработке очень важно полностью понимать, как переменные хранятся в JS. Для себя я стараюсь не передавать везде переменные ссылочного типа, я могу случайно изменить переменные в одном месте, а логика в другом месте не очень хорошо продумана, легко сделать ошибки, особенно когда сложность проекта высока, и При разработке с несколькими людьми. Вот почему я предпочитаю использовать чистые функции.

Кроме того, по моему предыдущему опыту собеседования, многие мелкие партнеры думают, что следующий код сообщит об ошибке, что также вызвано незнанием способа хранения переменных в JS.

// const 声明一个不可改变的变量。 
// a 存储的只是数组的内存地址而已,a.push 并不会改变 a 的值。
const a = [];
a.push('1'); 
console.log(a); // ['1']

Сборка мусора в JS

Алгоритмы сборки мусора в основном полагаются на концепцию ссылок. В контексте управления памятью говорят, что объект ссылается на другой объект, если у него есть разрешение (неявное или явное) на доступ к другому объекту. Например, объект Javascript имеет ссылки на свой прототип (неявные ссылки) и ссылки на свои свойства (явные ссылки).

Здесь понятие «объект» относится не только к объектам JavaScript, но и к области видимости функции (или глобальной лексической области видимости). Когда переменная больше не нужна, JS-движок вернет память, занятую переменной. Но как определить [переменная больше не нужна]? Есть два основных метода.

алгоритм подсчета ссылок

Упростите определение «не нужен ли объект больше», поскольку «есть ли у объекта ссылки на него из других объектов». Если ссылок на объект нет (ноль ссылок), объект будет утилизирован механизмом сборки мусора. Пример на MDN:

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
           // 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

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

// 这种情况下,o和o2都无法被回收。
function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}
f();

алгоритм маркировки-развертки

Этот алгоритм предполагает настройку объекта, называемого корнем (в Javascript корнем является глобальный объект). Сборщик мусора будет периодически запускаться в корне, находить все объекты, на которые ссылается корень, а затем находить объекты, на которые ссылаются эти объекты... Начиная с корня, сборщик мусора находит все объекты, которые он может получить, и собирает все объекты, которые он не может получить.

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

Утечки памяти в JS

Хотя JS автоматически обрабатывает выделение и переработку памяти за нас, в некоторых конкретных сценариях алгоритм сборки мусора JS не может помочь нам удалить память, которая больше не используется. Это явление, когда программа не может освободить память, которая больше не используется из-за небрежности или ошибки, называется утечкой памяти.

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

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

Вот пример кода с утечкой памяти:

class Page1 extends React.Component {

    events= []

    componentDidMount() {
        window.addEventListener('scroll', this.handleScroll.bind(this));
    }

    render() {
        return <div>
            <div><Link to={'/page2'}>前往Page2</Link></div>
            <p>page1</p>
      		  ....
        </div>
    }

    handleScroll(e) {
        this.events.push(e);
    }
}

Когда мы нажимаем кнопку, чтобы перейти на страницу 2, и непрерывно прокручиваем страницу 2, мы обнаружим, что использование памяти продолжает расти:

Причина этой утечки памяти: когда Page1 размонтирована, хотя Page1 и уничтожена, функция обратного вызова прокрутки Page1 все еще может быть «тронута» через eventListener, поэтому она не будет удалена сборщиком мусора. После входа на страницу Page2 логика события прокрутки все еще действует, и внутренние переменные не могут быть проверены сборщиком мусора. Если пользователь в течение длительного времени выполняет такие операции, как смахивание на странице 2, страница постепенно зависает.

Приведенные выше примеры не редкость в нашем процессе разработки. Не только привязка событий, но и логика отчетов о времени и т. д. Как это решить? Не забудьте выполнить соответствующую операцию отмены при размонтировании.

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

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

Ранее мы говорили о механизме сборки мусора JS, если мы держимЦитировать, то объект не будет собирать мусор. Ссылка здесь относится ксильная цитата.

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

В JS WeakMap и WeakSet предоставляют нам возможность слабой ссылки.

WeakMap, WeakSet

Чтобы поговорить о WeakMap, давайте сначала поговорим о Map. Объекты карты содержат пары ключ-значение и могут запоминать исходный порядок вставки ключей. Любое значение (объект или примитив) может использоваться как ключ или значение.

Карты являются сильными ссылками на объекты:

const m = new Map();
let obj = { a: 1 };
m.set(obj, 'a');
obj = null; // 将obj置为null并不会使 { a: 1 } 被垃圾回收,因为还有map引用了 { a: 1 }

WeakMap — это набор пар ключ/значение, где на ключи слабо ссылаются. Его ключи должны быть объектами, а значения могут быть произвольными. WeakMap — это слабая ссылка на объект:

const wm = new WeakMap();
let obj = { b: 2 };
wm.set(obj, '2');
obj = null; // 将obj置为 null 后,尽管 wm 依然引用了{ b: 2 },但是由于是弱引用,{ b: 2 } 会在某一时刻被GC。

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

WeakSet можно рассматривать как частный случай, когда все значения в WeakMap являются булевыми значениями, поэтому мы не будем их здесь повторять.

WeakMap в JavaScript делаетНе совсем слабая отсылка: На самом деле, пока ключ жив, он строго ссылается на свое содержимое. WeakMap слабо ссылается на свое содержимое только после того, как ключ был удален сборщиком мусора. Эту связь правильнее назвать ephemeron .

WeakRef

WeakRef — это API более высокого уровня, предоставляющий настоящие слабые ссылки. Давайте посмотрим на эффект WeakRef непосредственно с помощью приведенного выше примера с утечкой памяти:

import React from 'react';
import { Link } from 'react-router-dom';

// 使用WeakRef将回调函数“包裹”起来,形成对回调函数的弱引用。
function addWeakListener(listener) {
    const weakRef = new WeakRef(listener);
    const wrapper = e => {
        if (weakRef.deref()) {
            return weakRef.deref()(e);
        }
    }
    window.addEventListener('scroll', wrapper);
}

class Page1 extends React.Component {

    events= []

    componentDidMount() {
        addWeakListener(this.handleScroll.bind(this));
    }

    componentWillUnmount() {
        console.log(this.events);
    }

    render() {
        return <div>
            <div><Link to={'/page2'}>前往Page2</Link></div>
            <p>page1</p>
            ....
        </div>
    }

    handleScroll(e) {
        this.events.push(e);
    }
}


export default Page1;

Давайте посмотрим на производительность памяти после нажатия кнопки перехода на страницу 2:

Интуитивно видно, что после перехода на страницу 2 и прокрутки в течение определенного периода времени память остается стабильной. Это связано с тем, что когда page1 размонтирована, реальная функция обратного вызова прокрутки (функция handleScroll Page1) подвергается сборке мусора. Переменные внутри него также в конечном итоге подвергаются сборке мусора.

Но на самом деле здесь все же есть проблема, хотя мы и проходимweakRef.deref()Функция обратного вызова прокрутки handleScroll больше недоступна (была проверена сборщиком мусора), но наша оболочка функции-оболочки по-прежнему будет выполняться. Поскольку мы не реализовали removeEventListener. В идеале: мы хотим, чтобы прослушиватель прокрутки тоже был отменен.

можно использоватьFinalizationRegistryдля достижения этой функции. См. пример кода ниже:

// FinalizationRegistry构造函数接受一个回调函数作为参数,返回一个示例。我们把实例注册到某个对象上,当该对象被GC时,回调函数会触发。
const gListenersRegistry = new FinalizationRegistry(({ window, wrapper }) => {
    console.log('GC happen!!');
    window.removeEventListener('scroll', wrapper);
});

function addWeakListener(listener) {
    const weakRef = new WeakRef(listener);
    const wrapper = e => {
        console.log('scroll');
        if (weakRef.deref()) {
            return weakRef.deref()(e);
        }
    }
    // 新增这行代码,当listener被GC时,会触发回调函数。回调函数传参由我们自己控制。
    gListenersRegistry.register(listener, { window, wrapper });
    window.addEventListener('scroll', wrapper);
}

WeakRef и FinalizationRegistry — это высокоуровневые API, поддерживаемые начиная с Chrome v84 и Node.js 13.0.0. Как правило, не рекомендуется использовать. Поскольку его легко использовать неправильно, это может привести к большему количеству проблем.

написать на обороте

Эта статья начинается с управления памятью в JS и рассказывает о слабых ссылках в JS. Хотя движок JS помогает нам справляться с проблемами управления памятью, мы не можем полностью игнорировать проблемы с памятью при развитии бизнеса, особенно при разработке Node.js.

Для получения более подробной информации о стратегии памяти V8 вы можете перейти к статье, которую я перевел ранее:

Управление памятью двигателя V8

Использованная литература:

1,Woohoo.YouTube.com/watch?V=TPM…

2,Woohoo.info Q. Способность/статья/LK…

3.developer.Mozilla.org/this-cn/docs/…