js сценарии утечки памяти, как отслеживать и анализировать

JavaScript

Эта статья включена вgitbook.dasu.fun

В: Что такое утечка памяти?

Буквально запрошенная память не была вовремя восстановлена ​​и произошла утечка

Q: Почему происходит утечка памяти?

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

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

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

Итак, поговорим о том, какие сценарии вызовут утечку памяти

Что может вызвать утечку памяти

1. Неожиданная глобальная переменная

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

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

2. Забытый таймер

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

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

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

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

3. Неправильное использование затворов

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

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

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

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

4. Отсутствующие элементы DOM

Жизненный цикл элементов DOM обычно зависит от того, смонтированы ли они в дереве DOM.При удалении из дерева DOM они могут быть уничтожены и переработаны.

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

5. Обратный вызов сети

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

Как отслеживать утечки памяти

Утечки памяти можно разделить на две категории, одна более серьезная, утечка не может быть восстановлена ​​все время, а другая чуть менее серьезная, то есть утечки памяти, вызванные несвоевременной очисткой, все же можно устранить. через какое-то время.

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

Для первого, более серьезного, будет обнаружено, что общий объем используемой памяти продолжает расти даже после того, как GC продолжают появляться в карте памяти.

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

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

Сценарий 1. Подать заявку на часть памяти в функции, а затем функция вызывается непрерывно в течение короткого промежутка времени.

// 点击按钮,就执行一次函数,申请一块内存
startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
});

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

Все переменные, используемые внутри функции, являются локальными переменными.После выполнения функции эта память бесполезна и может быть переработана.

Итак, когда мы вызываем эту функцию непрерывно в течение короткого периода времени, мы можем обнаружить, что при выполнении функции обнаруживается, что памяти недостаточно, работает механизм сборки мусора, и память, запрошенная предыдущей функцией, утилизируется, потому что выполнение предыдущей функции закончилось, а память бесполезна и может быть переработана span

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

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

// 点击按钮,就执行一次函数,申请一块内存
var arr = [];
startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
    arr.push(b);
});

В чем разница с первой картинкой?

Это уже не горизонтальная линия, и нижняя часть каждой вертикальной линии в горизонтальной линии не находится на одном уровне, верно?

На самом деле это утечка памяти.

Мы подали заявку на два массива памяти в функции, но один из массивов удерживается извне, тогда, даже после каждого выполнения функции, эта часть внешней памяти массива все еще не может быть восстановлена, поэтому каждый раз только освобождать часть памяти

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

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

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

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

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

Как анализировать утечки памяти и находить проблемный код

Чтобы проанализировать причину утечек памяти, вам все еще нужно использовать функцию «Память» инструментов разработчика.Эта функция может делать снимки памяти, а также фиксировать выделение памяти за период времени и захватывать функции, запускающие выделение памяти за период времени. время состояние

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

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

Давайте сначала возьмем простой пример, а затем приведем пример фактической утечки памяти:

Сценарий 1: запросить часть памяти в функции, а затем функция вызывается непрерывно в течение короткого промежутка времени, но часть памяти, применяемой каждый раз, удерживается извне.

// 每次点击按钮,就有一部分内存无法回收,因为被外部 arr 持有了
var arr = [];
startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
    arr.push(b);
});
  • Снимок памяти

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

Вы также можете просмотреть снимок в определенный момент отдельно, и посмотреть, какие объекты занимают большой объем памяти из коэффициента заполнения памяти, как показано на следующем рисунке:

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

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

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

  • Возьмите период времени, выделение памяти

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

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

  • Получить использование памяти функцией за определенный период времени

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

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

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

Анализ случая

Вот пример утечки памяти, которая фигурировала во многих статьях в Интернете:

var t = null;
var replaceThing = function() {
  var o = t
  var unused = function() {
    if (o) {
      console.log("hi")
    }        
  }
 
  t = {
        longStr: new Array(100000).fill('*'),
        someMethod: function() {
                       console.log(1)
                    }
      }
}
setInterval(replaceThing, 1000)

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

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

  • проблема найдена

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

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

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

  • анализировать проблему

Теперь, когда вы определили наличие утечки памяти, пришло время выяснить причину утечки памяти.

Во-первых, с помощью профиля выборки мы обнаруживаем подозреваемого в функции replaceThing.

Затем мы получаем два снимка памяти и сравниваем их, чтобы увидеть, можем ли мы получить какую-либо информацию:

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

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

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

Начнем непосредственно со второго снимка памяти и посмотрим:

За период от первого снимка до второго снимка replaceThing выполнялся 7 раз, и было создано ровно 7 объектов, похоже, что эти объекты не были переработаны

Так почему же его не перерабатывают?

Функция replaceThing просто сохраняет последний объект внутри, но после завершения выполнения функции не должны ли локальные переменные быть переработаны?

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

Почему нельзя перерабатывать внутренне созданные объекты после каждого вызова функции replaceThing?

Из-за первого создания replaceThing этот объект хранится в глобальной переменной t, поэтому его нельзя повторно использовать.

Для каждого последующего вызова этот объект удерживается локальной переменной o внутри предыдущей функции replaceThing и не может быть повторно использован.

Локальная переменная o в этой функции хранится в методе someMethod объекта, созданного при первом вызове replaceThing.Объект, смонтированный этим методом, хранится в глобальной переменной t, поэтому он не может быть повторно использован.

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

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

盗自 https://juejin.cn/post/6844903488967852046

  • Завершение Заключение

В соответствии с использованием инструментов анализа памяти можно получить следующую информацию:

  1. Для одного и того же вызова функции использование памяти увеличивается ступенчато, и память не может быть уменьшена вручную GC, что указывает на утечку памяти.
  2. Возьмите ситуацию с приложением памяти в течение определенного периода времени, и можно определить, что подозрительной функцией является replaceThing
  3. Сравнивая снимки памяти, обнаруживается, что объекты, созданные внутри replaceThing (включая атрибут longStr и метод someMethod массива хранения), не перерабатываются.
  4. Дальнейший анализ моментального снимка памяти показал, что причина, по которой он не перерабатывается, заключается в том, что объект, созданный каждым вызовом функции, будет храниться в локальной переменной o, созданной внутри при последнем вызове функции.
  5. Локальная переменная o не перерабатывается в конце выполнения функции, поскольку она удерживается методом someMethod созданного объекта.

Вышесказанное является выводом, но мы должны проанализировать, почему это происходит, верно?

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

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

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

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

Другой момент знания заключается в том, что все функции, определенные во внешней функции, имеют общее замыкание, то есть функция b использует переменную внешней функции a, даже если функция c не используется, функция c все равно будет хранить переменную a, которая называется общим замыканием

вернуться к этому вопросу

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

Хотя someMethod не использует локальные переменные, внутри replaceThing есть неиспользуемая функция, которая использует локальную переменную o. Из-за общего замыкания someMethod также хранит o

А еще o хранит значение глобальной переменной t перед заменой, так что это приводит к тому, что каждый раз при вызове функции внутренняя переменная o будет кем-то удерживаться, поэтому ее нельзя будет переработать

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

Таким образом, есть две идеи: либо разрешить некоторому методу не сохранять о, либо освободить о после его использования;

Если неиспользуемая функция бесполезна, вы можете напрямую удалить эту функцию и увидеть эффект:

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

Или извлеките снимок памяти, чтобы увидеть, что при извлечении снимка он сначала автоматически принудительно выполнит сборку мусора, а затем извлечет снимок:

Да, даже если функция replaceThing вызывается периодически, даже если локальная переменная o в функции хранит значение последней глобальной переменной t, это все-таки локальная переменная.После выполнения функции, если нет внешней ссылки к нему, он может быть переработан, так что в итоге в памяти останется только объект, хранящийся в глобальной переменной t.

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

var unused = function() {
    if (o) {
      console.log("hi")
      o = null;
    }        
}

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

На самом деле, есть ли проблема с кодом, который следует здесь рассмотреть, зачем ему нужно хранилище локальных переменных, зачем ему существование неиспользуемой функции и для чего эта функция, если она используется только для определить, доступна ли последняя глобальная переменная t в какой-то момент в будущем, то почему бы не использовать глобальную переменную напрямую для хранения, зачем выбирать локальную переменную?

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

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


Чтобы прочитать больше вопросов для фронтенд-интервью, перейдите по ссылкеgitbook.dasu.fun