Как проверить приложение на утечку памяти
При разработке больших приложений, оперирующих большими объемами информации на первое место при отладке встает проблема обнаружения неправильного распределения памяти. Суть проблемы состоит в том, что если мы выделили участок памяти, а затем освободили не весь выделенный объем, то образуются блоки памяти, которые помечены как занятые, но на самом деле они не используются. При длительной работе программы такие блоки могут накапливаться, приводя к значительному расходу памяти.
Как обнаружить утечку памяти
Введение
При разработке больших приложений, оперирующих большими объемами информации на первое место при отладке встает проблема обнаружения неправильного распределения памяти. Суть проблемы состоит в том, что если мы выделили участок памяти, а затем освободили не весь выделенный объем, то образуются блоки памяти, которые помечены как занятые, но на самом деле они не используются. При длительной работе программы такие блоки могут накапливаться, приводя к значительному расходу памяти.
Для обнаружения подобных ошибок создано специализированное программное обеспечение (типа BoundsChecker от Numega), однако чаще бывает удобнее встроить механизм обнаружения утечки в свои проекты. Поэтому метод должен быть простым, и в то же время как можно более универсальным. Кроме того, не хотелось бы переписывать годами накопленные мегабайты кода, написанного и отлаженного задолго до того, как вам пришло в голову оградить себя от ошибок. Так что к списку требований добавляется стандартизация, т.е. нужно каким-то образом встроить защиту от ошибок в стандартный код.
Предлагаемое решение основывается на перегрузке стандартных операторов распределения памяти new и delete. Причем перегружать мы будем глобальные операторы new|delete, т.к. переписать эти операторы для каждого разработанного ранее класса было бы очень трудоемким процессом. Т.о. после перегрузки нам нужно будет только отследить распределение памяти и, соответственно, освобождение ее в момент завершения программы. Все несоответствия - ошибка.
Реализация
Проект написан на Visual C++, но переписать его на любой другой диалект С++ не будет слишком сложной задачей. Во-первых, нужно переопределить стандартные операторы new и delete так, чтобы это работало во всех проектах. Поэтому в stdafx.h добавляем следующий фрагмент:
Теперь все наши операторы new будут вызываться с тремя параметрами, причем недостающие параметры подставит препроцессор. Конечно, пустые переопределенные функции ни в чем нам не помогут, так что давайте добавим в них какой-нибудь код:
Для полноты картины нужно переопределить операторы new[] и delete[], однако никаких существенных отличий здесь нет - творите!
Последний штрих - пишем функции AddTrack() и RemoveTrack(). Для создания списка используемых блоков памяти будем использовать стандартные средства STL:
Надеюсь, этот проект сделает ваши баг-листы короче, а программы устойчивее. Удачи!
Вы должны быть уверены, что знаете обо всех пользовательских проблемах, как только они появляются в продакшене, а в идеале, до него. Одной из таких бед являются утечки памяти.
Когда это происходит, приложение постепенно замедляется и в какой-то момент работать с ним становится невозможно. В некоторых случаях это может замедлить работу веб-браузера и компьютера. Такие проблемы трудно отслеживать и отлаживать, поскольку они сразу не видны и проявляются только при длительной работе.Для отслеживания этих неприятностей можно использовать Sematext Experience – инструмент мониторинга, позволяющий не только контролировать использование веб-приложениями памяти, но также быстро и эффективно обнаруживать утечки. В статье мы подробно рассмотрим, как это делается.
Что такое утечка памяти
Утечка памяти – это избыточное потребление ресурсов из-за неправильного управления распределением памяти в программном обеспечении. Она происходит, когда веб-приложение выделяет память и продолжает использовать ее даже когда та больше не нужна.
Что вызывает утечку памяти: признаки, на которые следует обратить внимание
Веб-приложение будет создавать и использовать различные константы, переменные и функции для выполнения работы. Все они требуют памяти. В JavaScript и других языках программирования высокого уровня, когда ресурс больше не нужен, он будет удален автоматической системой управления памятью, называемой сборщиком мусора.
Память освобождается, когда она больше не выделена в коде. Однако бывают ситуации, когда память продолжает быть выделенной на протяжении всего жизненного цикла приложения. Иногда это необходимо, но если мы больше не нуждаемся в информации, а область памяти все еще выделена, можно говорить об утечке.Во фронтенд-приложениях существует несколько распространенных паттернов, которые могут вызвать утечку памяти. К ним относятся:
- Замыкания – внутренние функции, имеющие ссылки на переменные внешних. Таким образом внутренняя функция не позволяет сборщику мусора освободить память.
- Случайные глобальные переменные – ранее необъявленные переменные станут глобальными и не будут освобождены.
- Несвязанные коллекции – массивы, мапы и наборы позволяют хранить данные в каком-то виде, но с багами в коде они могут привести к утечке памяти из-за вечного хранения ссылок. Такой пример будет рассмотрен ниже.
- Отсоединенный DOM – элемент Document Object Model, который больше не используется, но на него продолжают ссылаться.
- Несвязанные таймеры – работающие вечно и сохраняющие объекты таймеры могут привести к утечке памяти.
Как обнаружить утечки памяти: пример приложения
Дальнейшая идея заключается в том, чтобы хранить все в бекенде, периодически отправляя данные. Однако чтобы создать утечку, мы забываем сделать одну штуку: после отправки данных в бекенд не очищаем массив. Это означает, что на данные по-прежнему ссылаются из нашего веб-приложения. Если пользователь не закрывает вкладку, данные будут продолжать добавляться, что приведет к утечке памяти.
Теперь посмотрим, как можно идентифицировать возникновение утечки памяти с помощью Sematext и его способности измерять использование памяти сайтом и приложением.
Интеграция Sematext Experience с веб-приложением
Первый шаг – настроить приложение на использование Sematext Browser SDK для отправки данных в облако Sematext.
Для этого добавим скрипт перед закрывающим тегом </head>:
Второй шаг зависит от типа вашего веб-приложения и используемого фреймворка – нужно сообщить Browser SDK, как ему себя настроить. Например, для стандартного развертывания нескольких веб-страниц вы бы использовали перед закрывающим тегом </head> что-то вроде этого:
Если вы используете какую-либо одностраничную архитектуру, инструкции по установке приложения Sematext Experience помогут выполнить все необходимые для отправки метрик шаги.
Выявление утечки памяти
Зная что делает пользователь, мы можем взглянуть на графики в Sematext Experience. Перейдите к отчету об использовании памяти в приложении:
Если бы у вас было подобное смоделированному веб-приложение с утечкой , вы могли бы наблюдать, как бесконечно растет потребление памяти. Не совсем бесконечно: рано или поздно работа браузера завершится аварийно . Диаграмма использования памяти должна быть привязана к некоторым значениям. Например, вот так:
Этот экран помогает сузить круг ошибок, показывая все действия пользователя: какие вызовы API были сделаны, какие части приложения выполнялись, какие ресурсы были загружены. Остается только изучить все части кода, и после дальнейшего исследования можно будет найти проблемные фрагменты.
Поддерживаемые браузеры
Возможности измерения памяти Sematext Browser SDK зависят от используемого браузером API для предоставления связанных с памятью метрик. Поддерживается он только в Chrome и проходил испытания Chrome Origin для Chrome 82-87. Начиная с Chrome 89 API будет полностью доступен — это означает, что пользователям потребуется новейший веб-браузер Google.
Заключение
С помощью правильных инструментов выявить проблемы с утечкой памяти довольно просто. Именно для этой цели был создан и постоянно совершенствуется Sematext Experience – он предоставляет вам комплексное, доступное и простое в использовании решение для мониторинга.
Утечка памяти достаточно серьезная проблема возникающая при работе программы. Масштабность проявляется особенно при длительной работе программы, когда программа может исчерпать лимит выделения для нее памяти, а это приведет к очень нехорошим последствиям.
Простейшая программа с утечкой памяти:
В данном участке кода утечка памяти происходит из-за отсутствия оператора delete[] для массива p.
В этой статье рассмотрены:
1 Обнаружение утечек с библиотекой CRT
Библиотека CRT доступна в операционной системе Windows, она у вас уже есть если вы используете Microsoft Visual Studio. Для подключения анализатора памяти достаточно добавить пару макросов и собрать проект в отладочном режиме:
Видно, что первая группа макросов добавляется в самое начало вашей программы. За счет этих макросов все обращения в функциям new, malloc, free и delete заменяются на другие версии, которые помимо выделения/освобождения памяти сохраняют дополнительную информацию (сколько и где было выделено). Макрос _CrtDumpMemoryLeaks добавляется в конец программы (перед завершением работы) и выводит информацию о текущем состоянии памяти.
Для приведенной выше программы в окно отладки будет выведено следующее:
Утечка была обнаружена, кроме того, нам показывают ее размер и даже строку кода где это случилось.
2 Использование Visual Leak Detector
Все говото, утечки ищутся и выводятся в консоль отладки:
Видно, что VLD выводит информацию о стеке вызовов, но формат вывода крайне неудобный. Узнать в какой строке произошла утечка памяти совсем непросто.
3 Работа с Valgrind memcheck
Valgrind предоставляет множество инструментов для динамического анализа, поиском утечек памяти это не ограничивается. Работой с памятью занимается модуль memcheck. Для его использования необходимо:
1) Сстановить valgrind:
sudo apt install valgrind
2) Скомпилировать программу в debug-режиме и запустить ее через valgrind:
valgrind ./my_program
При таком запуске будет выведено сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо добавить опцию
--leak-check=full .
Результаты работы memcheck для нашего примера:
4 Как это устроено внутри
Подключение CRT приводит к оборачиванию функций работы с памятью в что-то такое:
Да и зачем этим заниматься если его готовый CRT? В свою очередь, Visual Leak Detector представляет собой обертку над CRT.
4 Более сложный пример
Возможно, вам кажется что проблем с поиском утечек не возникает, тогда посмотрите такой пример:
Cat() ведь деструктор
Результаты применения всех трех рассмотренных инструментов примерно одинаковы (разве что VLD не выводит информацию о строке возникновения ошибки). Ниже приведен вывод valgring memcheck:
В статье мы расскажем, что такое утечка памяти, как происходит и какие вызывает последствия для операционной системы Android. Также рассмотрим инструменты для обнаружения утечек памяти, типовые модели утечки памяти в Android, способы оценки степени критичности и методы предотвращения основных видов утечек.
Каждому приложению для нормальной работы нужна оперативная память. Для обеспечения необходимым количеством памяти всех приложений Android должен эффективно управлять выделением памяти под каждый процесс. Среда выполнения Android запускает сборку мусора (GC), когда оперативная память заканчивается.
Что такое сборщик мусора?
Java Memory Management со встроенным сборщиком мусора является одним из лучших достижений этого языка. Он позволяет разработчикам создавать новые объекты, не заботясь о распределении памяти и ее освобождении, поскольку сборщик мусора автоматически восстанавливает память для повторного ее использования. Это обеспечивает более быструю разработку с меньшим количеством кода, одновременно устраняя утечки памяти и другие проблемы, связанные с ней. По крайней мере, в теории.
По иронии судьбы сборщик мусора Java работает слишком хорошо, создавая и удаляя большое количество объектов. Большинство проблем управления памятью решаются, но часто за счет уменьшения производительности. Создание универсального сборщика мусора, применяемого ко всем возможным ситуациям, привело к сложностям с оптимизацией системы. Чтобы разобраться со сборщиком мусора, нужно сначала понять, как работает управление памятью на виртуальной машине Java (JVM).
Как работает сборщик мусора
Многие считают, что сборщик мусора собирает и удаляет из памяти неиспользуемые объекты. На самом деле сборщик мусора Java делает все наоборот. Живые объекты отмечаются как активные, а все остальное считается мусором. Как следствие, эта фундаментальная особенность может привести ко многим проблемам с производительностью.
Начнем с так называемой кучи (англ. «heap») — области памяти, используемой для динамического распределения ресурсов приложений. В большинстве конфигураций операционная система заранее отдает эту часть под управление JVM во время работы программы. Это приводит к последствиям:
- создание объекта происходит быстрее, потому что глобальная синхронизация с операционной системой не требуется для каждого отдельного объекта. В процессе выделения памяти под приложение JVM просто фиксирует за задачей определенный участок памяти и перемещает указатель смещения вперед (картинка ниже). Следующее распределение начинается с этого смещения и занимает следующий участок памяти;
- когда объект больше не используется, сборщик мусора восстанавливает базовое состояние этого участка памяти и повторно использует ее для размещения другого объекта. Это означает, что нет явного удаления и память все еще не будет очищена.
Новые объекты просто размещаются в конце кучи.
Все объекты размещены в куче, управляемой JVM. Каждый элемент, используемый разработчиком, обрабатывается таким образом, включая объекты класса, статические переменные и даже сам код. Пока объект ссылается на что-то, JVM считает его используемым. Когда объект больше не ссылается и, следовательно, недоступен по коду приложения, сборщик мусора удаляет его и восстанавливает неиспользуемую память. Все настолько просто, как и звучит, но возникает вопрос: какова первая ссылка в дереве объектов?
Корни сборщика мусора — начальная позиция всех иерархий (деревьев) объектов
Каждое дерево объектов должно иметь один или несколько корневых объектов. Пока приложение может достичь этих корней, все дерево доступно. Но когда эти корневые объекты считаются доступными? Специальные объекты, называемые корнями сборщика мусора (корни GC, рисунок ниже), всегда доступны, а также любой объект, чьим корнем является корень сборщика мусора.
Sportmaster Lab , Москва, Санкт-Петербург, Новосибирск, можно удалённо , По итогам собеседования
В Java существуют следующие типы корней сборщика мусора:
- локальные переменные поддерживаются активными благодаря стеку потока. Это фиктивная виртуальная ссылка на объект и, следовательно, она не видна. Для всех целей и задач локальные переменные являются корнями сборщика мусора;
- активные потоки Java всегда считаются используемыми объектами и поэтому являются корнями сборщика мусора. Это особенно важно для локальных переменных потока;
- на статические переменные ссылаются их классы. Это делает их де-факто корнями сборщика мусора. Сами классы могут быть собраны сборщиком, что приведет к удалению всех статических переменных, на которые они ссылаются. Это имеет особо важно, когда мы используем серверы приложений, контейнеры OSGi или загрузчики классов в целом.
Корни сборщика мусора — это объекты, которые ссылаются на JVM и, таким образом, остаются в памяти устройства.
Поэтому простое Java-приложение имеет следующие корни сборщика мусора:
- локальные переменные в главном методе;
- основной поток;
- статические переменные главного класса.
Маркировка и сборка мусора
Чтобы определить, какие объекты больше не используются, JVM периодически запускает алгоритм маркировки и сборки мусора:
- Алгоритм «проходит» по всей иерархии объектов, начиная с корней сборщика мусора, и отмечает каждый найденный объект как активный.
- Вся участки памяти, не содержащие активных объектов (а точнее объектов, которые не были отмечены в предыдущем шаге), восстанавливаются. Они просто обозначаются как свободные.
Сборщик мусора предназначен для устранения причины утечки памяти — недостижимых, но не удаленных объектов в памяти. Однако это работает только для утечек памяти в классическом их понимании. Возможно, что неиспользуемые объекты по-прежнему доступны приложению, потому что разработчик просто забыл очистить ссылки на них. Такие объекты не могут быть собраны сборщиком. Хуже того, такая логическая утечка памяти не может быть обнаружена никаким программным обеспечением.
Простыми словами, в памяти остаются только те объекты, которые используются пользователем.
Однако, когда код написан плохо, неиспользуемые объекты могут ссылаться на несуществующие объекты, и сборщик мусора отмечает их как активные и не может их удалить. Это и называется утечкой памяти.
Почему утечка памяти — это плохо?
Ни один объект не должен оставаться в памяти дольше, чем нужно. Ведь эти ресурсы могут пригодиться для задач, которые могут иметь реальную ценность для пользователя. В частности, для Android это вызывает следующие проблемы:
Во-первых, когда происходят утечки, доступной для использования памяти становится меньше, что вызывает более частые запуски сборщика мусора. Такие запуски останавливают рендеринг пользовательского интерфейса, а также вызывают остановку других компонентов, необходимых для нормальной работы системы. В таких случаях прорисовка кадра длиться дольше обычных 16 мс. Когда прорисовка опускается до отметки ниже 100 мс, пользователи начнут замечать замедления в работе приложений.
В Android отзывчивость приложений контролируется менеджером активности и менеджером окон. Система откроет диалог ANR (приложение не отвечает) для конкретного приложения, когда будет выполнено одно из следующих условий:
- приложение не отвечает на нажатие клавиш, или нажатия на экран на протяжении 5 секунд;
- BroadcastReceiver не завершился на протяжении 10 секунд;
Во-вторых, приложение с утечкой памяти не сможет получить дополнительные ресурсы от неиспользуемых объектов. Оно сделает запрос на выделение дополнительной памяти, но всему есть свой предел. Android откажется выделять больше памяти для таких приложений. Когда это произойдет, приложение просто упадет. Это может вызвать негативные эмоции у пользователей, а они, в свою очередь, могут не только удалить приложение, но и оставить негативные отзывы о нем в магазине приложений.
Как определить утечку?
Чтобы определить утечку памяти, необходимо очень хорошо разбираться в работе сборщика мусора. Но Android также может предоставить несколько хороших инструментов, которые могут помочь определить возможные утечки или найти подозрительный кусок кода.
Приложение Leak Canary от Square — хороший инструмент для обнаружения утечек памяти в приложении. Оно создает ссылки на объекты вашего приложения и проверяет, удаляются ли эти ссылки сборщиком мусора. Если нет, тогда все данные записываются в файл .hprof и проводится анализ на наличие утечек памяти. Если утечка все же будет обнаружена, приложение пришлет вам уведомление о том, как это происходит. Рекомендуется использовать это приложение до выпуска в продакшн.Android Studio также имеет удобный инструмент для обнаружения утечек памяти. Если есть подозрения, что часть кода в вашем приложении может вызывать утечку, тогда можно сделать следующее:
- Скомпилировать и запустить отладочную версию сборки на эмуляторе или устройстве подключенному к вашему компьютеру;
- Перейти к подозрительной операции, затем вернуться к предыдущему действию, которое выведет подозрительную операцию из стека задач;
- В Android Studio открыть Android Monitor window → Memory section и нажать на кнопку запуска сборщика мусора (Initiate GC). Затем нажать кнопку Dump Java Heap ;
- После нажатия кнопки Dump Java Heap Android Studio откроет файл .hprof . Существует несколько способов проверки утечки памяти через этот файл. Вы можете использовать Analyzer Tasks в правом верхнем углу для автоматического обнаружения утечек. Или же можно переключиться в режим Tree View и найти действие, которое должно быть отключено. Проверяем данные Total Count , и если нашли отличия в данных, значит, что где-то есть утечка памяти.
- Как только была обнаружена утечка, нужно проверить дерево ссылок и узнать, какой объект ее вызывает.
Каковы общие схемы утечек?
Есть множество причин, по которым происходит утечка памяти в Android. Но все они могут быть отнесены к трем категориям.
- утечки памяти, инициируемые статической ссылкой;
- утечки памяти, инициируемые рабочим процессом;
- просто утечка.
Можно загрузить приложение SinsOfMemoryLeaks, которое поможет определить, где происходит утечка.
А теперь быстро пройдемся по всем видам утечек.
Утечки памяти, инициируемые статической ссылкой
Статическая ссылка сохраняется до тех пор, пока ваше приложение находится в памяти. У операций есть свои жизненные циклы, которые прекращаются и начинаются во время работы с приложением. Если вы обращаетесь к операции прямо или косвенно со статической ссылки, сборщик мусора не очистит занимаемую память после завершения операции. Память, занимаемая определенной операцией, может варьировать от нескольких килобайт до нескольких мегабайт в зависимости от того, в каком состоянии находится приложение. Если у него большая иерархия представлений или изображения с высоким разрешением, это может привести к утечке большого количества памяти.
Некоторые особенности утечек для этой категории:
Утечки памяти, инициируемые рабочим процессом
Рабочий поток также может работать дольше, чем нужно. Если сделать ссылку на операции прямо или косвенно из рабочего потока, который живет дольше, чем сами операции, это вызовет утечку памяти. Некоторые особенности утечек для этой категории:
Тот же принцип применяется к таким потокам, как thread pool или ExecutorService .
Просто утечка
Каждый раз при запуске рабочего потока из операции вы сами отвечаете за управление потоком. Поскольку рабочий поток может работать дольше самой операции, нужно остановить его, когда действие будет прекращено. Если этого не сделать, существует вероятность утечки памяти рабочего процесса. Как в этом репозитории.
Каково влияние конкретной утечки?
Насколько велика утечка памяти?
Не все утечки памяти одинаковые. Некоторые утечки могут составлять несколько килобайт, а некоторые — несколько мегабайт. Это можно определить, используя инструменты представленные выше и решить, имеет ли размер просочившейся памяти критическое значение для пользовательских устройств.
Как долго длится утечка?
Некоторые утечки через рабочий поток живут до тех пор, пока работает этот поток. В таком случае нужно изучить насколько долго живет этот поток. В примере приложения выше созданы бесконечные циклы в рабочем потоке, поэтому они постоянно держат в памяти объект, порождающий утечку. Но на самом деле большинство рабочих потоков выполняет простые задачи, такие как доступ к файловой системе или выполнение сетевых вызовов, которые либо недолговечны, либо ограничены тайм-аутом.
Сколько объектов в утечке?
В некоторых случаях утечку порождает только один объект, например, один из примеров статических ссылок, показанный в приложении SinsOfMemoryLeaks. Как только будет создано новое действие, оно начнет ссылаться на новую операцию. Старая утечка будет очищена сборщиком мусора. Таким образом, максимальная утечка всегда равна размеру одного экземпляра операции. Однако другие утечки продолжают просачиваться в новые объекты по мере их создания. В примере Leaking Threads активность пропускает по одному потоку каждый раз при его создании. Поэтому, если вы поворачиваете устройство 20 раз, утечка составит 20 рабочих потоков. Это закончится весьма печально, так как приложение заполнит всю доступную память на устройстве.
Посмотрите как происходит устранение типичных утечек памяти в этой ветке репозитория. Решения можно обобщить до следующих пунктов:
- Нужно быть очень осторожными, принимая решение установки статической переменной для рабочего процесса. Это действительно необходимо? Возможно, эта переменная ссылается на процесс напрямую или косвенно (ссылка на объект внутреннего класса, прикрепленный экран и т. д.)? Если да, возможно ли будет очистить отсылку к процессу, используя функцию onDestroy ?
- Если было решено передавать операцию как синглтон или x-manager , нужно понимать, что делает другой объект с экземпляром действия. Нужно очистить ссылку (установить в null), если необходимо, используя для этого процесса функцию onDestroy .
- При создании класса внутри процесса, по возможности старайтесь сделать его статическим. Внутренние классы и анонимные классы имеют неявную ссылку на родительский класс. Поэтому, если экземпляр внутреннего/анонимного класса живет дольше, чем родительский класс, могут возникнуть проблемы. Например, при создании анонимного класса runnable и передаче его в рабочий поток или класс анонимного обработчика и использования его для передачи задач в другой поток существует риск утечки содержащегося объекта класса. Чтобы избежать риска утечки, нужно использовать статический класс, а не внутренний/анонимный класс.
- Если писать синглтон или x-manager класс, нужно сохранить ссылку на экземпляр слушателя (англ. «listener»). При этом вы не контролируете, что происходит со ссылкой (удалил ее пользователь класса или нет). В этом случае можно использовать WeakReference для создания ссылки на экземпляр слушателя. WeakReference не мешает сборщику мусора производить свои действия. Хотя эта функция отлично подходит для предотвращения утечек памяти, она также может вызвать побочный эффект, потому что нет гарантии, что ссылочный объект является активным, когда это необходимо. Поэтому рекомендуется использовать его в качестве последнего средства для исправления утечек памяти.
- Всегда нужно завершать рабочие потоки, инициированные функцией onDestroy() .
Не забудьте проверить примеры кода для типичных утечек памяти и способы их избежания в репозитории на Github.
Читайте также: