🔥

Тред (Сергей Ufocoder)


JavaScript и утечки памяти в Браузере #тред
notion image

Жизненный цикл памяти практически всегда одинаков и не зависит от ЯП: - Аллокация памяти (allocate) - Использование (usage) - Высвобождение (release) JavaScript не позволяет напрямую работать с памятью, это происходит опосредованно, через синтаксические конструкции языка

1/2 Аллокация памяти происходит при инициализации значений
notion image

2/2 Скрытая аллокация памяти через вызовы функций
notion image

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

Высвобождение памяти в JavaScript происходит с помощью сборщика мусора
notion image

1/2 Аллоцированные объекты, можно представить в виде направленного графа, причем c одним корневым элементом. Как правило корень - это глобальный объект. В браузере такой корневой узел графа - это объект window Для следующего блока кода:
notion image

2/2 Получится вот такой граф
notion image

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

В ЯП с ручным управлением памятью: выделил память? воспользовался? убери за собой! Полная ответственность на разработчике В ЯП с автоматическим управлением памятью за вас уберет сборщик мусора (garbage collector) Полная ответственность на сборщике мусора.

В ЯП со сборщиками мусора, то что является мусором, зависит исключительно от алгоритмов реализованных в сборщике мусора. Для понимания этого, к рассмотрению предлагается 2 алгоритма сборки мусора: Reference Counting - посчет ссылок Mark-Sweep Collector - "пометь и выброси"

1/4 Возьмем следующий блок кода, создадим объекты, создадим ссылку внутри объект, а после "занулим" некоторые переменные
notion image

2/4 Граф объектов до зануления мы можем представить так, как на картинке Теперь рассмотрим что здесь будет являться мусором для каждого алгоритма
notion image

3/4 С точки зрения алгоритма Referece counting, если хотя бы у одного узла есть ссылка, значит его не нужно удалять из памяти, если ссылок нет - это мусор При таком подходе существует проблема циклических ссылок, разработчик намеренно удалил объект, сборщик не чистит - утечка
notion image

3/4 здесь после зануления мусором будет с2

4/4 Для алгоритма mark-and-sweep, мусором будет, те узлы, которые недостижимы из корневого. Для этого производится обход по графу (фаза mark), где помечаются достижимые узлы, после удаляются недостижимые (фаза sweep) По этому алгоритму мусором будут a1-a2 и c2
notion image

4/4 У сборщика мусора с реализацией этого алгоритма больше накладных расходов и есть одна серьезная проблема "проблема остановки мира", мир остановится пока не завершится его работа

Есть еще алгоритмы: Mark-compact garbage collection, Copying garbage collection, Comparing garbage collectors, Generational garbage collection и другие Разработка сборщика мусора - отдельное искусство с собственным списком решаемых задач

Источников по этой теме немного, но они есть и они хорошие The Garbage Collection Handbook (платно): gchandbook.org/contents.html Курсы и статьи про компиляторы и сборщики мусора у Дмитрия Сошникова: dmitrysoshnikov.com

Вернемся к JavaScript и Браузеру

В современных браузерах сборщики мусора (GC) реализованы на поколениях, используют идею достижимости алгоритма mark-and-sweep и прочее Например, Webkit: webkit.org/blog/7122/intr…
notion image

А что нужно знать JavaScript разработчику, помимо достижимости объектов из корневого глобального объекта window? Сборщик мусора недетерминирован! Может запуститься в любом момент, его работа не контролируется, не считая возможностей devtools браузера

@jsunderhood Ещё один неплохой источник для начала dev.to/deepu105/demys… Есть перевод на хабре, но не для всех частей. habr.com/ru/post/489360/
Дополнение по источникам касательно работы с памятью twitter.com/zelarky/status…

Когда-то эмперически было выясненно, что как правило GC запускается после очередной из аллокаций (как сейчас дела обстоят не знаю). Отсюда можно написать такой JS код, который ни разу не запустит GC. Как? Предварительно аллоцировав все необходимые объекты html5rocks.com/en/tutorials/s…

Перейдем к практике. У современных браузеров (chrome, safari) существуют инструменты для профилирования этой кучи, а также новых аллокаций. developers.google.com/web/tools/chro…

Идея поиска утечек на практике проста: - выделяем пользовательский сценарий - растет ли память в процессе? - должна ли она расти? - ищем объекты, которые аллоцируются - используются ли они? - нет? утечка!

Chrome devtools. Performace profiling. Важным здесь является профилирование Memory. Оно покажется количество объектов в JS Heap, documents, listeners, nodes и тд. Вспомните ранее показанный граф объектов, как стало ясно они бывают разных типов.
notion image

Помимо возможности вызвать вручную сборку мусора, можно также проследить за работой этого сборщика мусора, в части можно обнаружить фазы его работы. Бывает Major GC, Minor JS и как видно фаза работает не за один раз На скрине я вручную запустил сборку мусора.
notion image

Major GC и Minor GC это намек на то, что сборщик мусора в v8 работает на поколениях. В прикреплении детали работы сборщика мусора Orinoco в v8 Trash talk: the Orinoco garbage collector v8.dev/blog/trash-talk youtube.com/watch?v=Scxz6j…

Если график профилирования памяти выглядит так (картинка), значит все хорошо, память аллоцируется, затем срабатывает сборщик мусора. Общее значение используемой памяти колеблется около одного уровня - утечки памяти нет.
notion image

Если график профилирования памяти выглядит так (картинка), GC пытается чистить, но общий уровень растет, значит возможна есть утечка памяти. Однако утечка это или нет зависит от того какая задача заложена в скрипт, мб аллокации и подразумеваются задачей.
notion image


Хорошо, память растет, возможно утечка, что делать дальше? Искать 🙃 Heap Snapshot позволит сделать снимки кучи (место, где все объекты хранятся, вспомните граф) и сравнить снимки м/у собой, этот инструмент позволит понять КАКИЕ объекты "увеличивают память"

Allocation Timeline, другой инструмент, позволит понять КОГДА и ЧТО аллоцировалось developers.google.com/web/tools/chro…
notion image

Какие еще есть инструменты, что они делают, как ими пользоваться - читайте в описании Google DevTools В статьях скриншоты devtools могут быть устаревшими, так как devtools постоянно развивается, однако ничего страшного. developers.google.com/web/tools/chro…

Обычно выделяют 4 типа утечек связанных с: - глобальными переменными - таймерами - event listener - замыканиями Популярная статья об утечках памяти, помимо всего прочего показывает какие типы утечек бывают: blog.sessionstack.com/how-javascript…

В своем старом доклад про утечки памяти выделял больше типов. Поэтому вопрос типов утечек - вопрос относительный Слайды: slides.com/xufocoder/memo…
notion image

Напоследок приведу пример самой простой утечки из доклада. Напишем сервис кэширования. Также для демонстрации роста памяти будем "сохранять" дополнительные данные. Создадим его экземпляр и вызовем много раз метод cache
notion image

График Performace profile, на нем видно несколько "всплексов", это я вручную запускал GC, однако количество памяти (объектов в JS Heap) не уменьшилось, а сервис в примере выше был удален, тоесть память должна очиститься, объекты остались в памяти - нашли утечку.
notion image

В чем проблема? Проблема в переменной cache она хранит в себе значения, после удаления сервиса, поскольку является глобальным объектом Решение в лоб, сделать переменную локальной
notion image

Сделав очередные замеры, и запустив вручную GC, получаем следующий график. После вызовов GC количество объектов в JS Heap возвращается к изначальному уровню. Утечка устранена
notion image

@jsunderhood Наверное, в контексте кэширования стоит упомянуть довольно уникальные WeakMap и WeakSet.
Как заметил @anber_ru второе решение это использование Weak-* объектов, в нашем случае WeakMap, который будет хранить ссылки слабо, то есть если на объект больше не ссылаются другие объекты, кроме WeakMap, тогда этот объект удаляется twitter.com/anber_ru/statu…