что такое memleak detect
Утечка памяти
Когда мы пишем код, то создаём различные объекты, которые занимают память. Когда объект нам не нужен, то его нужно уничтожить, чтобы освободить память для других объектов. Этим занимается специальный сборщик мусора (garbage collector). Но иногда программа написана таким образом, что сборщик мусора думает, что объект вам ещё нужен и не удаляет его из памяти. Тем самым кусок памяти остаётся занятым. А если процесс создания новых объектов с неправильным поведением повторяется неоднократно, то память просто забивается. В конце концов приложение может израсходовать лимит выделяемой памяти. Это состояние и называют утечкой памяти, т.е. приложению было выделено определённое количество памяти, а на самом деле используется меньшее количество. Откат, распил бюджета, коррупция. В этом случае приложение перестаёт работать, зависает и падает с ошибкой.
Данная статья является компиляцией из разных источников. Собрал в одном месте, чтобы немного упорядочить информацию.
Стек работает в порядке LIFO (Last In, First Out), то есть последний добавленный в стек фрагмент данных будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, переменная добавляется в стек, а когда эта переменная пропадает из области видимости, она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных. Размер стека — это фиксированная величина, и превышение лимита выделенной на стеке памяти приведёт к переполнению стека. Размер задаётся при создании потока, и у каждой переменной есть максимальный размер, зависящий от типа данных.
Куча — это хранилище памяти, также расположенное в ОЗУ, которое допускает динамическое выделение памяти. Куча не имеет упорядоченного набора данных, это просто склад для ваших переменных. По завершении приложения все выделенные участки памяти освобождаются. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически, и это позволяет создавать динамические переменные.
Чтобы наглядно преддставить способ хранения объектов в памяти, напишем простую программу на Java.
Размещение в памяти при запуске выглядит следующим образом.
По рисунку видно, что в стек попали функция main(), переменная с примитивным типом int.
Также в стек попадает объект obj, когда он создаётся из класса Object, при этом в куче создаётся ссылка на класс (указатель).
Аналогично, в стеке появляется объект mem с ссылкой на класс в куче.
Для функции foo() в стеке создаётся отдельный блок. В этом блоке создаётся объект param с ссылкой в куче на класс Object и строковый объект с ссылкой в куче на отдельный блок String Pool.
Когда в программе выполнение доходит до закрывающей фигурной скобкой метода foo(), метод прекращает работу и объекты в стеке, относящиеся к блоку функции, освобождаются. Память выглядит следующим образом.
Последняя закрывающая фигурная скобка от функции main() закрывает эту функцию, освобождая свой блок данных.
Существуют различные методики обнаружения утечек памяти и способы борьбы с ней. Вам нужно научиться устранять типовые утечки памяти.
Сначала ответим на вопрос: а зачем исправлять эти ошибки, чем это грозит? Даже с утечками памяти приложение может работать.
Основные проблемные источники: Context и его производные (Activity), внутренние классы (Inner Classes), анонимные классы (Anonymous Classes), Handlers c Runnable, Threads, TimerTask, SensorManager и другие менеджеры.
Например, хочется передать объект из одной активности в другой. Некоторые программисты создают статическую переменную для первой активности и обращаются к ней из второй. Это крайне неудачный подход. Не только потому, что он моментально приводит к утечке памяти (статическая переменная продолжит существовать пока существует приложение, и объект Activity, на который она ссылается, никогда не будет выгружен). Этот подход также может привести к ситуации, когда вы будете обмениваться информацией не с тем экраном, ведь экран, невидимый пользователю, может в любой момент быть уничтожен и создан заново, когда пользователь к нему вернётся.
Почему же утечка активности такая большая проблема? Дело в том, что если сборщик мусора не соберёт Activity, то он не соберёт и все View и Fragment, а вместе с ними и все прочие объекты, расположенные в Activity. В том числе не будут высвобождены картинки. Поэтому утечка любой активности — это, как правило, самая большая утечка памяти, которая может быть в вашем приложении.
Используйте передачу объектов через Intent, либо передавайте ID объекта (если у вас есть база данных, из которой этот id потом можно достать).
Этот пункт также относится к любым объектам, временем жизни которых напрямую или косвенно управляет Android, т.е. View, Fragment, Service и т.д.
Объекты View и Fragment содержат ссылку на Activity, в котором они расположены, поэтому, если утечёт один единственный View, утечёт сразу всё — Activity и все View в ней. И заодно все drawable и всё, на что у любого элемента из экрана есть ссылка!
Будьте аккуратны при передаче ссылки на Activity (View, Fragment, Service) в другие объекты.
Утечка через слушателей
Рассмотрим простой пример: ваше приложение для социальной сети отображает фамилию, имя и рейтинг текущего пользователя на каждом экране приложения. Объект с профилем текущего пользователя существует с момента входа в аккаунт до момента выхода из него, и все экраны вашего приложения обращаются за информацией к одному и тому же объекту. Этот объект также периодически обновляет данные с сервера, так как рейтинг может часто меняться. Необходимо, чтобы объект с профилем уведомлял текущую активность об обновлении рейтинга. Как этого добиться? Очень просто:
Как добиться в этой ситуации утечки памяти? Тоже очень несложно! Просто забудьте отписаться от уведомлений в методе onPause():
Из-за такой утечки памяти активность будет продолжать обновлять интерфейс каждый раз, когда профиль будет обновляться даже после того, как экран перестанет быть видим пользователю. Хуже того, таким образом экран может подписать 2, 3 или больше раза на одно и то же уведомление. Это может привести к видимым тормозам интерфейса в момент обновления профиля — и не только на этом экране.
Что делать, чтобы избежать этой ошибки? Во-первых, конечно нужно всегда внимательно следить за тем, что вы отписались от всех уведомлений в момент ухода Activity в фон.
Вы можете сохранять не ссылки на объекты, а слабые ссылки. Это особенно полезно для наследников класса View — ведь у них нет метода onPause() и не совсем понятно, в какой момент они должны отписываться от уведомления. Слабые ссылки не считаются сборщиком мусора как связи между объектами, поэтому объект, на который существуют только слабые ссылки, будет уничтожен, а ссылка перестанет ссылаться на объект и примет значение null.
Другой пример с использованием системных слушателей. Например, есть слушатель определения местоположения.
Если забудем снять регистрацию слушателя в onStop(), то пользователь может закрыть приложение, но сборщик мусора не сможет освободить память, так как LocationManager будет по-прежнему выполнять свою работу.
Пример утечки с внутренним классом
Часто в состав основного класса включают внутренний класс. Это нормально, но в некоторых случаях может стать причиной утечки памяти. Внутренний класс содержит ссылку на основной класс, но может иметь свой жизненный цикл.
Сам пример вполне нормальный. Но нужно помнить, что класс BackgroundTask хранит ссылку на активность. Если задача выполняется очень долго (плохое соединение, большая картинка на сервере), то сложная активность со всеми своими ресурсами остаётся в памяти, пока задача не будет отработана.
Есть разные варианты решения задачи. Часто рекомендуют подход с WeakReference.
Код усложнился, кроме того, вам придётся изучать устройство WeakReference.
Для примера на Kotlin можно убрать модификатор inner.
Если в качестве внутреннего класса использовать Handler, то студия будет выводить подсказку This Handler class should be static or leaks might occur (anonymous android.os.Handler). Код, чтобы увидеть подсказку.
Более подробное описание подсказки в студии:
Пример утечки с анонимным классом
Принцип утечки памяти схож с примером с внутренним классом, когда сохраняется ссылка на активность. Экземпляр анонимного класса живёт дольше, чем контейнер. Если анонимный класс вызывает какой-то метод, читает или записывает свойство в класс-контейнер, то он держит в памяти класс-контейнер.
Утечка через потоки
В обычной ситуации пользователь запустит активность, запустится задача на двадцать секунд.
Когда задача выполнится, стек освободит объекты.
Затем сборщик мусора освободит объекты в куче.
Когда пользователь закроет активность, основной метод будет будет освобождён и активность также будет удалена из кучи. Мы возвращаемся к начальной позиции.
Рассмотрим случай, когда пользователь закроет активность или повернёт экран после десяти секунд.
Задача по-прежнему выполняется, ссылка на активность по-прежнему жива и мы имеем утечку памяти.
Когда метод run() выполнится, стек освободит объекты и сборщик мусора в порядке очереди почистит объекты в куче, так как они уже не будут иметь ссылок из стека.
После поворота устройства 5 раз мы можем наблюдать картину, как утекает память.
Singleton (Одиночка)
Когда происходит утечка памяти? Когда мы инициализируем синглтон в активности, то передаём ссылку на контекс-активность с долгим сроком жизни.
В этом случае ссылка на активность будет существовать пока не закроется приложение.
Чтобы избежать утечку, используйте контекст приложения, а не активности.
Или вы можете переписать класс одиночки.
Утечка с таймерами
Таймеры, которые не отменяются при выходе с экрана, тоже служат источниками утечки памяти.
Вы разработали приложение для социальной сети. В этом приложении можно обмениваться сообщениями между пользователями, и вы добавляете на экран обмена сообщениями таймер, который делает запрос на сервер каждые 10 секунд с целью получения новых сообщений, но вы забыли этот таймер выключить при выходе с экрана:
К сожалению, эту проблему сложно избежать. Единственные два совета, которые можно дать, будут такими же, как и в предыдущем пункте: будьте внимательны и периодически проверяйте приложение на утечки памяти. Вы также можете использовать аналогичный предыдущему пункту подход с использованием слабых ссылок.
Фрагменты
Никогда не сохраняйте ссылки на Fragment в активности или другом фрагменте.
Активность хранит ссылки на 5-6 запущенных фрагментов даже если на экране всегда виден только один. Один фрагмент хранит ссылку на другой фрагмент. Фрагменты, видимые на экране в разное время, общаются друг с другом по прямым закешированным ссылкам. FragmentManager в таких приложениях выполняет чаще всего рудиментарную роль — в нужный момент он подменяет содержимое контейнера нужным фрагментом, а сами фрагменты в back stack не добавляются (добавление фрагмента, на который у вас есть прямая ссылка, в back stack рано или поздно приведёт к тому, что фрагмент будет выгружен из памяти; после возврата к этому фрагменту будет создан новый, а ваша ссылка продолжит ссылаться на существующий, но невидимый пользователю фрагмент).
Это очень плохой подход по целому ряду причин. Во-первых, если вы храните в активности прямые ссылки на 5-6 фрагментов, то это тоже самое, как если бы вы хранили ссылки на 5-6 Activity. Весь интерфейс, все картинки и вся логика пяти неиспользуемых фрагментов не могут быть выгружены из памяти, пока запущено Activity.
Во-вторых, эти фрагменты становится крайне сложно переиспользовать. Попробуйте перенести фрагмент в другое место программы при условии, что он должен быть обязательно запущен в одном Activity с фрагментами, x, y и z, которые переносить не надо.
Относитесь к фрагментам как к Activity. Делайте их максимально модульными, общайтесь между фрагментами только через Activity и FragmentManager.
Утечки памяти, связанные с неправильным использованием android.os.Handler. Не совсем очевидно, но все, что вы помещаете в Handler, находится в памяти и не может быть очищено сборщиком мусора в течении некоторого времени. Иногда довольно длительного. Читайте статью Борьба с утечками памяти в Android. Часть 1
Пример утечки с системными менеджерами
В Android есть много системных менеджеров (содержат слово «Manager» в именах классов), которые следует регистрировать. И часто программисты забывают снять регистрацию.
Возьмём для примера класс LocationManager, который помогает определить местоположение. Напишем минимальный код.
Запустите пример. В студии внизу выберите вкладку 6: Android Monitor (Сейчас вместо него появился Profiler), а в ней вкладку Monitors. В верхней части окна будет блок Memory, который представляет для нас интерес.
Начинайте вращать устройство с запущенным приложением. Вы увидите, что ваше приложение начинает забирать память у устройства (тёмно-синий цвет).
Мне не удалось исчерпать всю память и сломать приложение, в какой-то момент умная система освобождала занятую память и всё повторялось снова. В других ситуациях может случиться так, что память кончится раньше, чем сообразит система.
Нажмите на третью кнопку в этом окне Dump Java Heap. Данное действие сгенерирует hprof-файл, содержащий слепок памяти в заданный момент. Далее студия автоматически откроет созданный файл, который можно изучить.
Обратите внимание на вкладку Analyzer Tasks сбоку в верхнем правом углу. Откройте эту вкладку. В ней вы увидите строчку с флажком Detect Leaked Activities (Обнаружить утекающие активности). В окнеAnalysis Results щёлкните по строке Leaked Activities, чтобы увидеть дополнительную информацию.
Видно, что при поворотах создавалось множество активностей MainActivity, а вместе с ней и объект LocationManager.
Добавим код в метод onDestroy(), как это предписано документацией.
Запустите приложение снова и начинайте вращать устройство. Сделайте дамп памяти для анализа. Вы увидите, что теперь активность не утекает. Могут остаться другие проблемы, влияющие на потребление памяти, но свою проблему мы решили. Поэтому не забывайте освобождать ресурсы, если об этом просят в документации.
Запустите профайлер, появится окно с четырьмя блоками: CPU, MEMORY, NETWORK, ENERGY. Нас интересует память. Щёлкаем в этой области, чтобы оставить слежение только за используемой памятью.
Нажмите кнопку Dump Java heap, чтобы получить дамп кучи. Рядом имеется кнопка очистки мусора Force garbage collection.
Утечки памяти в C++: что это такое и чем они опасны
Разбираемся в трудно уловимых уязвимостях приложений, чтобы всё работало гладко и без тормозов.
Утечка памяти (англ. memory leak) — это неконтролируемое уменьшение свободной оперативной или виртуальной памяти компьютера. Причиной утечек становятся ошибки в программном коде.
В этой статье мы поймём, как появляются утечки и как их устранять.
Как появляются утечки памяти
Любые программы используют в своей работе память, чтобы хранить какие-то данные. В C++ и многих других языках память динамическая. Это значит, что операционная система при запуске программы резервирует какое-то количество ячеек в ОЗУ, а потом выделяет новые, если они нужны.
Создали переменную для числа (int)? Вот тебе 16 бит (4 байта). Нужен массив из ста элементов для больших чисел (long)? Вот тебе ещё 3200 бит (800 байт).
Когда программисту уже не нужен какой-то массив или объект, он должен сказать системе, что его можно удалить с помощью оператора delete[] и освободить память.
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Однако иногда случаются ошибки, которые приводят к утечкам памяти. Вот одна из них:
На примере в цикле десять раз создаётся новый массив, а его адрес записывается в указатель. Адреса старых массивов при этом удаляются. Поэтому дальше оператор delete[] удаляет только последний созданный массив. Остальные останутся в памяти до тех пор, пока не будет закрыта программа.
Чем опасны утечки памяти
Когда приложение съест всю доступную память, сработает защита ОС и ваша программа аварийно закроется. Однако у утечек могут быть и более опасные последствия.
Например, приложение может работать с каким-нибудь файлом непосредственно перед закрытием. В этом случае файл будет повреждён. Последствия возможны самые разные: от нервного срыва пользователя, если это была презентация, над которой он работал несколько дней, до поломки системы, если это был очень важный файл.
В отдельных случаях утечка памяти одного приложения может привести к последствиям для других работающих приложений. Например, если ваш код изменил или занял память, используемую другой программой.
Может показаться, что раз это «утечка», то что-то случится с вашими данными. На самом деле утекает именно свободная память, а не её содержимое.
Как бороться с утечками памяти
Если у вас есть доступ к исходникам, то изучите код, чтобы определить, нет ли там утечек. Вручную делать это бессмысленно, особенно если проект большой, поэтому обратимся к отладчику использования памяти (англ. memory debugger).
Если вы пользуетесь IDE вроде Visual Studio, то там должен быть встроенный отладчик. Есть и сторонние инструменты вроде GDB или LLDB. Отладчик покажет, какие данные хранит программа и к каким ячейкам имеет доступ.
Ловим утечки памяти в С/С++
Приветствую вас, Хабровчане!
Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.
На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.
Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.
Для понимания, что происходит, прикладываю реальный пример:
А также есть Student.h и Student.c в котором объявлены структуры и функции.
Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.
В начало файла, где находится main, необходимо добавить этот кусок кода:
В итоге, в режиме Debug, студия будет выводить это:
Супер! Теперь вы знаете, что у вас утечка памяти. Теперь нужно устранить это, поэтому необходимо просто узнать, где мы забываем очистить память. И вот тут возникает проблема: а где, собственно, выделялась эта память?
После того, как я повторил все шаги, я выяснил, что память теряется где-то здесь:
Но как так — то? Я же все освобождаю? Или нет?
И тут мне сильно не хватало Valgrind, с его трассировкой вызовов.
В итоге, после 15 минут прогугливания, я нашел аналог Valgrind — Visual Leak Detector. Это сторонняя библиотека, обертка над CRT, которая обещала показывать трассировку! Это то, что мне необходимо.
Чтобы её установить, необходимо перейти в репозиторий и в assets найти vld-2.5.1-setup.exe
Правда, последнее обновление было со времен Visual Studio 2015, но оно работает и с Visual Studio 2019. Установка стандартная, просто следуйте инструкциям.
Преимущество этой утилиты заключается в том, что можно не запускать в режиме debug (F5), ибо все выводится в консоль. В самом начале будет выводиться это:
И вот, что будет выдавать при утечке памяти:
Вот, я вижу трассировку! Так, а где строки кода? А где названия функций?
Ладно, обещание сдержали, однако это не тот результат, который я хотел.
Остается один вариант, который я нашел в гугле: моментальный снимок памяти. Он делается просто: в режиме debug, когда доходите до return 0, необходимо в средстве диагностики перейти во вкладку «Использование памяти» и нажать на «Сделать снимок». Возможно, у вас будет отключена эта функция, как на первом скриншоте. Тогда необходимо включить, и перезапустить дебаг.
После того, как вы сделали снимок, у вас появится под кучей размер. Я думаю, это сколько всего было выделено памяти в ходе работы программы. Нажимаем на этот размер. У нас появится окошко, в котором будут содержаться объекты, которые хранятся в этой куче. Чтобы посмотреть подробную информацию, необходимо выбрать объект и нажать на кнопку «Экземпляры представления объекта Foo».
Да! Это победа! Полная трассировка с местоположением вызовов! Это то, что было необходимо изначально.
Linux — разработка
Теперь, посмотрим, что творится в Linux.
В Linux существует утилита valgrind. Чтобы установить valgrind, необходимо в консоли прописать sudo apt install valgrind (Для Debian-семейства).
Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:
Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.
Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.
Выводы
Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.
Спасибо вам за внимания, удачного написания кода вам!
4 вида утечек памяти в JavaScript и как с ними бороться
В этой статье мы рассмотрим распространённые виды утечек памяти в клиентском JavaScript. Также мы узнаем, как их обнаружить с помощью Chrome Development Tools.
Примечание переводчика: первая часть статьи насыщена примечаниями переводчика. В ходе анализа материала стало понятно, что некоторые моменты стоит отдельно пояснить.
Вступление
Утечки памяти принадлежат к тому роду задач, с которыми рано или поздно сталкивается каждый разработчик. Языки с автоматическим управлением памятью не решают все проблемы разом — всё равно существуют ситуации, когда память утекает. Утечки порождают целый класс различных проблем: задержки в работе пользовательского интерфейса, зависания и даже проблемы работы с другими приложениями.
Что такое утечка памяти?
Утечка памяти — память, которая больше не требуется приложению, но по какой-то причине не возвращается операционной системе или пулу доступной памяти (примечание переводчика: в кучу). Языки программирования используют разные подходы, снижающие риск возникновения утечек памяти, однако сама задача о том, понадобится ли ещё определенный фрагмент памяти или нет, алгоритмически неразрешима (примечание переводчика: она сводится к проблеме остановки). Иными словами, только разработчик может определить, возможно ли вернуть определенный фрагмент памяти операционной системе. Управление памятью в языках программирования делится на ручное и автоматическое. Первый тип предоставляет разработчику набор инструментов, помогающих напрямую взаимодействовать с памятью. Во втором существует специальный процесс, называемый «сборщиком мусора» (англ. garbage collector), вызываемый автоматически и удаляющий память.
Управление памятью в JavaScript
JavaScript — язык программирования со встроенным сборщиком мусора. Сборщик периодически проверяет, какие из выделенных приложению фрагментов памяти остаются «достижимы» из различных частей этого приложения. Иными словами, сборщик мусора переводит вопрос «какая память до сих пор нужна?» в вопрос «к какой памяти можно обратиться?». Разница кажется незначительной, однако это не так: хотя лишь разработчик знает, потребуется ли фрагмент выделенной памяти в будущем или нет, недостижимую память можно вычислить алгоритмически и пометить к возвращению в ОС.
Языки, не имеющие сборщиков мусора, работают по другим принципам. Например, существует явное управление памятью: разработчик напрямую говорит компилятору, что данный фрагмент памяти можно удалить. Также существует алгоритм подсчёта ссылок, при котором с каждым блоком памяти ассоциируется количество его использований (и когда оно обнулится, блок возвращается в ОС). Эти техники имеют свои плюсы и минусы, и могут привести к утечкам памяти.
Примечание переводчика: алгоритм подсчёта ссылок используется и в сборщиках мусора. Кроме того, стоит заметить, что работа этого алгоритма в базовом виде может привести к возникновению циклических ссылок, при котором неиспользуемые объекты ссылаются друг на друга, взаимно блокируя удаление. Подробнее — в Википедии.
Утечки памяти в JavaScript
Главной причиной утечек памяти в языках со сборщиками мусора являются нежелательные ссылки. Чтобы понять, что это такое, давайте сначала рассмотрим, как именно сборщик мусора проверяет достижимость объектов.
Алгоритм пометок (Mark-and-sweep)
Большинство сборщиков мусора используют алгоритм пометок (mark-and-sweep):
Сборщик рекурсивно обходит корни и их потомков, помечая их как активные (т.е. не мусор). Всё, до чего можно добраться из корня, не рассматривается в качестве мусора.
Современные сборщики мусора улучшают этот алгоритм, но его суть остаётся прежней: пометить достижимые фрагменты памяти, а остальное объявить мусором. Теперь можно дать определение нежелательным ссылкам — это ссылки, достижимые из корня, но ссылающиеся на фрагменты памяти, которые точно никогда больше не понадобятся. В JavaScript нежелательными ссылками станут потерявшие актуальность переменные, забытые в коде, удерживающие в памяти ненужные более объекты. Кстати, некоторые считают, что это ошибки разработчиков, а не языка.
Итак, чтобы выяснить, из-за чего обычно возникают утечки памяти в JavaScript, мы должны понять, какие ситуации приводят к возникновению нежелательных ссылок.
Примечание переводчика: перед тем, как читать дальше, рекомендую посмотреть статью MDN об управлении памятью, более подробно раскрывающую тему.
Четыре самых распространённых вида утечек памяти в JavaScript
1: Случайные глобальные переменные
Одной из целей, стоявших при разработке JavaScript, было создать похожий на Java язык, но настолько нестрогий, чтобы с ним могли работать даже новички. Одним из послаблений языка стала обработка необъявленных переменных: обращение к такой переменной создаст новую переменную в глобальном объекте. Таким образом, если рассмотреть код:
На самом деле он означает:
Ещё один способ создать случайную глобальную переменную — использовать this :
Чтобы избежать подобных ошибок, добавляйте ‘use strict’ ; в начало JavaScript-файлов. Это директива, включающая строгий режим парсинга JavaScript, препятствующий возникновению случайных глобальных переменных.
Замечание о глобальных переменных
Поговорим не о случайных, а о явно объявленных глобальных переменных. По определению они не обрабатываются сборщиком мусора, если только не приравнять их к null или переназначить. В частности, это касается глобальных переменных, использующихся для временного хранения и обработки больших блоков данных. Если вам нужна глобальная переменная, чтобы записать в неё большое количество информации, убедитесь, что в конце работы с данными её значение будет установлено в null или переопределено.
Примером увеличенного расхода памяти, связанным с глобальными переменными, являются кэши — объекты, которые сохраняют повторно используемые данные. Для эффективной работы их следует ограничивать по размеру. Если кэш увеличивается без ограничений, он может привести к высокому расходу памяти, поскольку его содержимое не может быть очищено сборщиком мусора.
2: Забытые таймеры и коллбэки
Довольно часто встречается подобное использование функции setInterval :
Поговорим о коллбэках. Чаще всего они используются в обработчиках событий и в сторонних библиотеках. Библиотеки обычно создают собственные обработчики событий и другие вспомогательные инструменты, обрабатывающие коллбэки. Обычно они также предоставляют способы удаления внешних обработчиков после того, как объект становится недостижимым.
Рассмотрим теперь ситуацию с обработчиками событий. Обработчики следует удалять, когда они становятся не нужны, или ассоциированные с ними объекты становятся недоступны. В прошлом это было критично, так как некоторые браузеры (Internet Explorer 6) не умели грамотно обрабатывать циклические ссылки (см. заметку ниже). Большинство современных браузеров удаляет обработчики событий, как только объекты становятся недостижимы. Однако по-прежнему правилом хорошего тона остаётся явное удаление обработчиков событий перед удалением самого объекта. Например:
Заметка об обработчиках событий и циклических ссылках
Обработчики событий и циклические ссылки издавна считались проблемой JavaScript-разработчиков. Это было связано с ошибкой (или дизайнерским решением) сборщика мусора в Internet Explorer. Старые версии Internet Explorer не могли обнаружить циклические ссылки между DOM-элементами и JavaScript кодом. Добавим к этому, что в обработчиках событий обычно содержится ссылка на объект события (как в примере выше). Это означает, что каждый раз, когда в Internet Explorer на DOM-узел добавлялся слушатель, возникала утечка памяти. Поэтому веб-разработчики начали явно удалять обработчики событий до удаления DOM-узлов или обнулять ссылки внутри обработчиков. Современные браузеры (включая Internet Explorer и Microsoft Edge) используют алгоритмы, находящие циклические ссылки и правильно их обрабатывающие. Теперь не обязательно вызывать removeEventListener перед удалением узла.
Фреймворки и библиотеки, такие, как jQuery, убирают обработчики перед тем, как удалить сам узел, если для их создания использовался библиотечный API. Это делается самими библиотеками и гарантирует отсутствие утечек, даже при работе с проблемными браузерами, вроде старого Internet Explorer.
3: Ссылки на удалённые из DOM элементы
Иногда полезно хранить DOM-узлы внутри структур данных. Предположим, вы хотите точечно обновить содержимое нескольких строк в таблице. Имеет смысл сохранить ссылку на каждый DOM-ряд в словаре или массиве. В этом случае на один и тот же DOM-элемент будут указывать две ссылки — одна в DOM-дереве, а вторая в словаре. Если в будущем вы решите удалить эти строки, вам понадобится сделать и ту, и другую ссылку недостижимыми.
В дополнении к этому нужно что-то делать со ссылками на внутренние узлы DOM-дерева. Предположим, что мы храним в коде ссылку на какую-то конкретную ячейку таблицы (на тег
4: Замыкания
Основополагающей частью JavaScript являются замыкания: функции, получающие переменные из родительских областей видимости. Разработчики Meteor обнаружили ситуацию, при которой из-за особенностей реализации среды исполнения JavaScript можно создать утечку памяти подобным хитрым способом:
При работе этого кода можно наблюдать постоянное увеличение используемой памяти. Объём памяти не уменьшается даже когда в дело вступает сборщик мусора. По сути у нас создаётся список связанных замыканий (с корнем в виде переменной theThing ), и в каждой области видимости этих замыканий содержится прямая ссылка на большую строку, что представляет собой значительную утечку памяти. Это артефакт реализации. С иной реализацией замыканий потенциально можно обработать эту ситуацию, что и объясняется в блоге Meteor-а.
Неочевидное поведение сборщиков мусора
Хотя сборщики мусора и полезны, у них есть свои недостатки, одним из которых является недетерминированность. Это значит, что сборщики мусора непредсказуемы — обычно невозможно определить, когда будет произведена сборка мусора. Как следствие, иногда программа занимает больше памяти, чем требуется. Также могут наблюдаться кратковременные паузы, что будет особенно заметно в быстро реагирующих на действия программах.
Недетерминированность означает, что мы не можем точно сказать, когда будет произведена сборка мусора, однако большинство реализаций сборщиков мусора имеют сходное поведение. Если не производится выделений памяти, сборщик мусора никак себя не проявляет. Расмотрим следующий сценарий:
В этом случае большинство сборщиков мусора не будет производить дальнейших действий. Иными словами, хоть и существуют недостижимые ссылки, которые можно обработать, сборщик мусора их не затронет. За счёт таких незначительных утечек приложение будет расходовать больше памяти, чем нужно. Google привели отличный пример подобного поведения — JavaScript Memory Profiling docs, example #2.
Обзор инструментов профилирования в Chrome
Chrome предоставляет набор инструментов для профилирования расхода памяти в JavaScript. Для работы с памятью предназначены два важнейших инструмента: вкладка timeline и вкладка профилей.
Вкладка timeline
Вкладка timeline неоценима для обнаружения необычного поведения памяти. При поиске больших утечек обратите внимание на периодические скачки, незначительно уменьшающиеся после сборки мусора. На скриншоте видно непрерывный рост вызывающих утечку памяти объектов. Даже после большой зачистки в конце, общее количество занимаемой памяти больше, чем вначале. Также возрастает количество DOM-узлов. Всё указывает на то, что в коде утечка, связанная с DOM-узлами.
Вкладка профилей
Работе с этой вкладкой вы посвятите большую часть времени. Профили позволяют делать снапшоты памяти и сравнивать их между собой. Также можно записывать процесс распространения памяти. В любом из режимов доступны разные типы вывода результатов, но более всего нам интересны общий список и список сравнения.
Общий список предоставляет обзор разных типов связанных объектов и совокупность их размеров: shallow size (поверхностный размер, сумму всех объектов конкретного типа) и retained size (удерживаемый размер, поверхностный размер плюс размер других объектов, связанных с данным). Также это даёт нам представление о том, насколько далёк объект от своего корня (поле distance).
Список сравнения предоставляет нам ту же информацию и даёт возможность сопоставить разные снапшоты. Это особенно важно для поисков утечек памяти.
Пример: Ищем ошибки с помощью Chrome
Есть два основных вида утечек памяти: утечки, вызывающие периодические увеличения используемой памяти и одиночные утечки, не вызывающие дальнейших увеличений памяти. Очевидно, проще всего отследить периодические утечки. Кроме того, они наиболее опасны: если расходуемая память постоянно увеличивается, в конце концов такие утечки замедлят работу браузера или остановят выполнение скрипта. Непереодические же утечки легко найти, если они достаточно велики, чтобы распознать их среди прочих. Обычно они не доставляют серьёзных проблем, поэтому зачастую остаются необнаруженными. Утечки, случающиеся лишь однажды, могут быть рассмотрены лишь как задачи оптимизации. А вот периодические утечки — это полноценные баги, которые надо устранять.
Функция grow при вызове начнёт создавать узлы
Для языков со сборщиками мусора характерны колебания в графике работы памяти. Это ожидаемо, если распространение памяти циклично, как обычно и случается. Мы рассмотрим периодические увеличения памяти, которые не возвращаются к исходному состоянию после отработки сборщика мусора.
Как обнаружить периодическое увеличение памяти
График JavaScript-кода также показывает постоянное увеличение расходуемой памяти. Его сложнее распознать из-за работы сборщика мусора. Вы можете увидеть, как изначально увеличивается память, затем следует её уменьшение, а затем опять увеличение и скачок, за которым следует очередное уменьшение памяти, и т.д. Важным в данной ситуации является то, что после каждой очистки памяти её общий размер всё равно остаётся больше предыдущего. То есть, хотя сборщику мусора и удаётся освободить значительное количество памяти, всё равно какая-то часть регулярно утекает.
Итак, теперь ясно, что у нас утечка. Давайте найдём её.
Сделайте два снапшота
Чтобы найти утечку, переместимся в раздел profile. Чтобы объём памяти можно было контролировать, перезагрузите страницу. Нам понадобится функция Take Heap Snapshot.
Есть два способа отследить распространение памяти в промежутке между двумя снапшотами. Можно выбрать Summary и затем кликнуть правой кнопкой на Objects allocated between Snapshot 1 and Snapshot 2 или вместо Summary выбрать Comparison. В обоих случаях мы увидим список объектов, возникших между двумя снимками.
Мы видим, что выбранное выделение памяти является частью массива. В свою очередь, на массив ссылается переменная x из глобальной области видимости. Это означает, что существует путь из выбранного объекта до корня — значит, объект нельзя очистить сборщиком мусора. Мы нашли потенциальную утечку. Отлично. Но пример был прост — редко встречаются настолько большие увеличения расхода памяти. Также видны утечки DOM-узлов, занимающие меньший объём. С помощью этих снапшотов легко обнаружить эти узлы, однако сделать это на больших сайтах будет труднее. Chrome предоставляет дополнительный инструмент, лучше всего справляющийся с данной задачей — функцию Record Heap Allocations.
Ищем утечки с помощью Record Heap Allocations
Если вы установили брейкпоинт, уберите его, позволив скрипту работать дальше. Вернитесь в панель профилей в инструментах разработчика. Нажмите Record Allocation Timeline. Во время работы этого инструмента вы увидите синие скачки в графике сверху, отображающие выделение памяти. Каждую секунду в коде происходит большое выделение памяти. Позвольте скрипту отработать несколько секунд, затем остановите его (не забывайте устанавливать брейкпоинты, иначе Chrome займёт всю память).
На снимке видно, чем хорош этот инструмент: можно выбрать отрезок таймлайна и посмотреть, какие выделения памяти произошли за данный период. Постараемся максимально приблизить один из скачков. В списке будут показаны только три конструктора: один из них связан с большими утечками ( (string) ), следующий — с выделением памяти для DOM-узлов, а последний — конструктор Text (создающий содержимое DOM-узла).
Выберите один из конструкторов HTMLDivElement из списка и нажмите Allocation stack.
Ещё одно полезное свойство
Также можно выбрать режим Allocation вместо Summary:
Комбинация описанных выше инструментов поможет вам искать утечки памяти. Используйте их. Пробуйте разные режимы профилирования на своих сайтах (лучше проверять не минимизированный и не обфусцированный код). Посмотрим, сможете ли вы найти утечки, или объекты, занимающие больше памяти, чем требуется (это будет сложнее).
Дополнительные материалы
Заключение
Даже в языках со встроенными сборщиками мусора, таких, как JavaScript, возникают утечки памяти. Иногда они могут остаться незамеченными, а иной раз приводят к катастрофе. Важно понимать, как работает управление памятью. Существуют специальные инструменты для анализа распространения памяти и поиска ошибок. Профилирование памяти должно стать частью цикла разработки, особенно для приложений средних и больших размеров. Начните это делать, чтобы не усложнять жизнь вашим пользователям.
- что значит донт ноу
- что делать после зачисления в вуз на бюджет в 2021