Утечка памяти что это
Утечки памяти в Android: что это такое, как обнаружить и предотвратить
Авторизуйтесь
Утечки памяти в Android: что это такое, как обнаружить и предотвратить
В статье мы расскажем, что такое утечка памяти, как происходит и какие вызывает последствия для операционной системы Android. Также рассмотрим инструменты для обнаружения утечек памяти, типовые модели утечки памяти в Android, способы оценки степени критичности и методы предотвращения основных видов утечек.
Каждому приложению для нормальной работы нужна оперативная память. Для обеспечения необходимым количеством памяти всех приложений Android должен эффективно управлять выделением памяти под каждый процесс. Среда выполнения Android запускает сборку мусора (GC), когда оперативная память заканчивается.
Что такое сборщик мусора?
Java Memory Management со встроенным сборщиком мусора является одним из лучших достижений этого языка. Он позволяет разработчикам создавать новые объекты, не заботясь о распределении памяти и ее освобождении, поскольку сборщик мусора автоматически восстанавливает память для повторного ее использования. Это обеспечивает более быструю разработку с меньшим количеством кода, одновременно устраняя утечки памяти и другие проблемы, связанные с ней. По крайней мере, в теории.
По иронии судьбы сборщик мусора Java работает слишком хорошо, создавая и удаляя большое количество объектов. Большинство проблем управления памятью решаются, но часто за счет уменьшения производительности. Создание универсального сборщика мусора, применяемого ко всем возможным ситуациям, привело к сложностям с оптимизацией системы. Чтобы разобраться со сборщиком мусора, нужно сначала понять, как работает управление памятью на виртуальной машине Java (JVM).
Как работает сборщик мусора
Многие считают, что сборщик мусора собирает и удаляет из памяти неиспользуемые объекты. На самом деле сборщик мусора Java делает все наоборот. Живые объекты отмечаются как активные, а все остальное считается мусором. Как следствие, эта фундаментальная особенность может привести ко многим проблемам с производительностью.
Начнем с так называемой кучи (англ. «heap») — области памяти, используемой для динамического распределения ресурсов приложений. В большинстве конфигураций операционная система заранее отдает эту часть под управление JVM во время работы программы. Это приводит к последствиям:
Новые объекты просто размещаются в конце кучи.
Все объекты размещены в куче, управляемой JVM. Каждый элемент, используемый разработчиком, обрабатывается таким образом, включая объекты класса, статические переменные и даже сам код. Пока объект ссылается на что-то, JVM считает его используемым. Когда объект больше не ссылается и, следовательно, недоступен по коду приложения, сборщик мусора удаляет его и восстанавливает неиспользуемую память. Все настолько просто, как и звучит, но возникает вопрос: какова первая ссылка в дереве объектов?
Корни сборщика мусора — начальная позиция всех иерархий (деревьев) объектов
Каждое дерево объектов должно иметь один или несколько корневых объектов. Пока приложение может достичь этих корней, все дерево доступно. Но когда эти корневые объекты считаются доступными? Специальные объекты, называемые корнями сборщика мусора (корни GC, рисунок ниже), всегда доступны, а также любой объект, чьим корнем является корень сборщика мусора.
В Java существуют следующие типы корней сборщика мусора:
Корни сборщика мусора — это объекты, которые ссылаются на JVM и, таким образом, остаются в памяти устройства.
Поэтому простое Java-приложение имеет следующие корни сборщика мусора:
Маркировка и сборка мусора
Чтобы определить, какие объекты больше не используются, JVM периодически запускает алгоритм маркировки и сборки мусора:
Сборщик мусора предназначен для устранения причины утечки памяти — недостижимых, но не удаленных объектов в памяти. Однако это работает только для утечек памяти в классическом их понимании. Возможно, что неиспользуемые объекты по-прежнему доступны приложению, потому что разработчик просто забыл очистить ссылки на них. Такие объекты не могут быть собраны сборщиком. Хуже того, такая логическая утечка памяти не может быть обнаружена никаким программным обеспечением.
Когда объекты больше не ссылаются прямо или косвенно на корень сборщика мусора, они будут удалены. Как видно, с классическими утечками памяти хорошо справляется встроенный сборщик мусора. С другими видами утечек памяти поможет справиться другое программное обеспечение, которое будет рассмотрено далее.
Простыми словами, в памяти остаются только те объекты, которые используются пользователем.
Однако, когда код написан плохо, неиспользуемые объекты могут ссылаться на несуществующие объекты, и сборщик мусора отмечает их как активные и не может их удалить. Это и называется утечкой памяти.
Почему утечка памяти — это плохо?
Ни один объект не должен оставаться в памяти дольше, чем нужно. Ведь эти ресурсы могут пригодиться для задач, которые могут иметь реальную ценность для пользователя. В частности, для Android это вызывает следующие проблемы:
Во-первых, когда происходят утечки, доступной для использования памяти становится меньше, что вызывает более частые запуски сборщика мусора. Такие запуски останавливают рендеринг пользовательского интерфейса, а также вызывают остановку других компонентов, необходимых для нормальной работы системы. В таких случаях прорисовка кадра длиться дольше обычных 16 мс. Когда прорисовка опускается до отметки ниже 100 мс, пользователи начнут замечать замедления в работе приложений.
В Android отзывчивость приложений контролируется менеджером активности и менеджером окон. Система откроет диалог ANR (приложение не отвечает) для конкретного приложения, когда будет выполнено одно из следующих условий:
Вряд ли пользователям понравится видеть это сообщение на экранах своего гаджета.
Во-вторых, приложение с утечкой памяти не сможет получить дополнительные ресурсы от неиспользуемых объектов. Оно сделает запрос на выделение дополнительной памяти, но всему есть свой предел. Android откажется выделять больше памяти для таких приложений. Когда это произойдет, приложение просто упадет. Это может вызвать негативные эмоции у пользователей, а они, в свою очередь, могут не только удалить приложение, но и оставить негативные отзывы о нем в магазине приложений.
Как определить утечку?
Чтобы определить утечку памяти, необходимо очень хорошо разбираться в работе сборщика мусора. Но Android также может предоставить несколько хороших инструментов, которые могут помочь определить возможные утечки или найти подозрительный кусок кода.
Каковы общие схемы утечек?
Есть множество причин, по которым происходит утечка памяти в Android. Но все они могут быть отнесены к трем категориям.
Можно загрузить приложение SinsOfMemoryLeaks, которое поможет определить, где происходит утечка.
В ветке Leak будут видны причины утечки памяти. Это приложение можно также запустить на устройстве или эмуляторе и использовать вышеупомянутые инструменты для отслеживания утечек. В ветке FIXED можно увидеть советы, как исправить утечки. После исправления процедуру можно повторить заново, чтобы окончательно убедиться в том, что утечки исправлены. Каждая из веток приложения имеет разные идентификаторы приложений, поэтому вы можете установить их на одном устройстве и проверять показания одновременно.
А теперь быстро пройдемся по всем видам утечек.
Утечки памяти, инициируемые статической ссылкой
Статическая ссылка сохраняется до тех пор, пока ваше приложение находится в памяти. У операций есть свои жизненные циклы, которые прекращаются и начинаются во время работы с приложением. Если вы обращаетесь к операции прямо или косвенно со статической ссылки, сборщик мусора не очистит занимаемую память после завершения операции. Память, занимаемая определенной операцией, может варьировать от нескольких килобайт до нескольких мегабайт в зависимости от того, в каком состоянии находится приложение. Если у него большая иерархия представлений или изображения с высоким разрешением, это может привести к утечке большого количества памяти.
Некоторые особенности утечек для этой категории:
Утечки памяти, инициируемые рабочим процессом
Рабочий поток также может работать дольше, чем нужно. Если сделать ссылку на операции прямо или косвенно из рабочего потока, который живет дольше, чем сами операции, это вызовет утечку памяти. Некоторые особенности утечек для этой категории:
Просто утечка
Каждый раз при запуске рабочего потока из операции вы сами отвечаете за управление потоком. Поскольку рабочий поток может работать дольше самой операции, нужно остановить его, когда действие будет прекращено. Если этого не сделать, существует вероятность утечки памяти рабочего процесса. Как в этом репозитории.
Каково влияние конкретной утечки?
В идеале следует избегать написания кода, который может вызвать утечку памяти, и исправить все утечки, существующие в приложении. Но на самом деле, если нужно работать со старой базой кода и определить приоритеты задач, включая исправление утечек памяти, можно оценить степень серьезности в следующих аспектах.
Насколько велика утечка памяти?
Не все утечки памяти одинаковые. Некоторые утечки могут составлять несколько килобайт, а некоторые — несколько мегабайт. Это можно определить, используя инструменты представленные выше и решить, имеет ли размер просочившейся памяти критическое значение для пользовательских устройств.
Как долго длится утечка?
Некоторые утечки через рабочий поток живут до тех пор, пока работает этот поток. В таком случае нужно изучить насколько долго живет этот поток. В примере приложения выше созданы бесконечные циклы в рабочем потоке, поэтому они постоянно держат в памяти объект, порождающий утечку. Но на самом деле большинство рабочих потоков выполняет простые задачи, такие как доступ к файловой системе или выполнение сетевых вызовов, которые либо недолговечны, либо ограничены тайм-аутом.
Сколько объектов в утечке?
В некоторых случаях утечку порождает только один объект, например, один из примеров статических ссылок, показанный в приложении SinsOfMemoryLeaks. Как только будет создано новое действие, оно начнет ссылаться на новую операцию. Старая утечка будет очищена сборщиком мусора. Таким образом, максимальная утечка всегда равна размеру одного экземпляра операции. Однако другие утечки продолжают просачиваться в новые объекты по мере их создания. В примере Leaking Threads активность пропускает по одному потоку каждый раз при его создании. Поэтому, если вы поворачиваете устройство 20 раз, утечка составит 20 рабочих потоков. Это закончится весьма печально, так как приложение заполнит всю доступную память на устройстве.
Как исправить и предотвратить утечки
Посмотрите как происходит устранение типичных утечек памяти в этой ветке репозитория. Решения можно обобщить до следующих пунктов:
Не забудьте проверить примеры кода для типичных утечек памяти и способы их избежания в репозитории на Github.
Борьба с утечками памяти в веб-приложениях
Когда мы перешли от разработки веб-сайтов, страницы которых формируются на сервере, к созданию одностраничных веб-приложений, которые рендерятся на клиенте, мы приняли определённые правила игры. Одно из них — аккуратное обращение с ресурсами на устройстве пользователя. Это значит — не блокировать главный поток, не «раскручивать» вентилятор ноутбука, не сажать батарею телефона. Мы обменяли улучшение интерактивности веб-проектов, и то, что их поведение стало больше похоже на поведение обычных приложений, на новый класс проблем, которых не существовало в мире серверного рендеринга.
Одна из таких проблем — утечки памяти. Плохо сделанное одностраничное приложение может легко сожрать мегабайты или даже гигабайты памяти. Оно способно занимать всё больше и больше ресурсов даже тогда, когда тихо сидит на фоновой вкладке. Страница такого приложения, после захвата им непомерного количества ресурсов, может начать сильно «тормозить». Кроме того, браузер способен просто завершить работу вкладки и сказать пользователю: «Что-то пошло не так».
Что-то пошло не так
Конечно, сайты, которые рендерятся на сервере, тоже могут страдать от проблемы утечки памяти. Но тут речь идёт о серверной памяти. При этом весьма маловероятно то, что такие приложения вызовут утечку памяти на клиенте, так как браузер очищает память после каждого перехода пользователя между страницами.
Тема утечек памяти не очень хорошо освещена в публикациях по веб-разработке. И, несмотря на это, я почти уверен в том, что большинство нетривиальных одностраничных приложений страдает от утечек памяти — если только команды, которые ими занимаются, не имеют надёжных инструментов для обнаружения и исправления этой проблемы. Дело тут в том, что в JavaScript крайне просто случайно выделить некий объём памяти, а потом просто забыть эту память освободить.
Автор статьи, перевод которой мы сегодня публикуем, собирается поделиться с читателями своим опытом по борьбе с утечками памяти в веб-приложениях, а также хочет привести примеры их эффективного обнаружения.
Почему об этом так мало пишут?
Для начала хочу поговорить о том, почему об утечках памяти так мало пишут. Полагаю, тут можно обнаружить несколько причин:
Анатомия утечки памяти
Современные библиотеки и фреймворки для разработки веб-приложений, такие, как React, Vue и Svelte, используют компонентную модель приложения. В рамках этой модели самый распространённый способ вызова утечки памяти выглядит примерно так:
Вот как решить эту проблему:
Ситуации, в которых чаще всего возникают утечки памяти
Опыт подсказывает мне, что утечки памяти чаще всего случаются при использовании следующих API:
Идентификация утечек памяти
Теперь мы перешли к решению непростой задачи по идентификации утечек памяти. Начну с того, что я не считаю, что какой-либо из существующих инструментов очень хорошо для этого подходит. Я пробовал инструменты анализа памяти Firefox, пробовал средства из Edge и IE. Испытывал даже Windows Performance Analyzer. Но лучшими из подобных инструментов всё ещё остаются Инструменты разработчика Chrome. Правда, и в этих инструментах есть множество «острых углов», о которых стоит знать.
Инструмент Heap snapshot позволяет делать снимки памяти главного потока, веб-воркеров или элементов iframe
Делаем первый снимок, далее — выполняем действия, которые могут вызвать утечку памяти, а потом делаем ещё один снимок. Если утечки нет — размеры выделенной памяти будут равны
Правда, Heap snapshot — далеко не идеальный инструмент. У него есть некоторые ограничения, о которых стоит знать:
Пробиваемся сквозь информационный шум
Я обнаружил, что лучший способ пробиться сквозь информационный шум — это многократное повторение действий, которые, как предполагается, вызывают утечку памяти. Например, вместо того, чтобы лишь один раз открыть и закрыть модальное окно после захвата первого снимка, это можно сделать 7 раз. Почему 7? Да хотя бы потому, что 7 — это заметное простое число. Затем надо сделать второй снимок и, сравнив его с первым, выяснить, «утёк» ли некий объект 7 раз (или 14 раз, или 21 раз).
Сравнение снимков кучи. Обратите внимание на то, что мы сравниваем снимок №3 со снимком №6. Дело в том, что я сделал три снимка подряд для того, чтобы Chrome провёл бы больше сеансов сборки мусора. Кроме того, обратите внимание на то, что некоторые объекты «утекли» по 7 раз
Ещё один полезный приём заключается в том, чтобы, в самом начале исследования, до создания первого снимка, один раз выполнить процедуру, в ходе которой, как ожидается, возникает утечка памяти. Особенно это рекомендуется в том случае, если в проекте применяется разделение кода. В подобном случае весьма вероятно то, что при первом выполнении подозрительного действия будет произведена загрузка необходимых JavaScript-модулей, что отразится на объёме выделенной памяти.
Сейчас у вас может возникнуть вопрос о том, почему вы должны обращать особое внимание на количество объектов, а не на общий объём памяти. Тут можно сказать, что мы интуитивно стремимся к тому, чтобы уменьшить объём «утекающей» памяти. В этой связи можно подумать о том, что следовало бы следить за общим объёмом используемой памяти. Но такой подход, по одной важной причине, подходит нам не особенно хорошо.
Если нечто «утекает», то происходит это из-за того, что (пересказывая Джо Армстронга) вам нужен банан, но вы, в итоге, получаете банан, гориллу, которая его держит, и ещё, в придачу, все джунгли. Если ориентироваться на общий объём памяти, то это будет то же самое, что «измерять» джунгли, а не интересующий нас банан.
Если анализировать разницу между снимками, сортируя сущности по объёму занимаемой ими памяти, это позволит увидеть множество массивов, строк, объектов, большинство из которых, скорее всего, не имеют отношения к утечке. А нам ведь надо найти тот самый прослушиватель событий, с которого всё началось. Он же, в сравнении с тем, на что он ссылается, занимает очень мало памяти. Для того чтобы исправить утечку, нужно найти банан, а не джунгли.
В результате, если сортировать записи по количеству «утёкших» объектов, то можно будет заметить 7 прослушивателей событий. И, возможно, 7 компонентов, и 14 подкомпонентов, и, может, ещё нечто подобное. Это число 7 должно выделяться из общей картины, так как это, всё же, довольно заметное и необычное число. При этом неважно то, сколько именно раз будет повторено подозрительное действие. При исследовании снимков, если подозрения оправданы, будет зафиксировано именно столько «утёкших» объектов. Именно так можно быстро обнаружить источник утечки памяти.
Анализ дерева ссылок
Инструмент для создания снимков памяти даёт возможность просматривать «цепочки ссылок», которые помогают узнать о том, на какие объекты ссылаются другие объекты. Это — то, что позволяет приложению функционировать. Анализируя подобные «цепочки» или «деревья» ссылок, можно выяснить то, где именно была выделена память под «утекающий» объект.
Цепочка ссылок позволяет выяснить то, какой объект ссылается на «утекающий» объект. Читая эти цепочки, нужно учитывать то, что объекты, расположенные в них ниже, ссылаются на объекты, расположенные выше
Стоит отметить, что инструмент для создания снимков кучи имеет некоторые ограничения:
Правда, надо сказать, что это руководство по поиску утечек памяти освещает лишь малую часть того, что происходит в реальности. Это лишь начало работы. Помимо этого нужно уметь обращаться с установкой точек останова, с логированием, с тестированием исправлений на предмет выяснения того, решают ли они проблему. И, к сожалению, всё это, по сути, выливается в серьёзные затраты времени.
Автоматизация анализа утечек памяти
Я хочу начать этот раздел с того, что мне не удалось найти хорошего подхода к автоматизации обнаружения утечек памяти. В Chrome есть собственный API performance.memory, но, из соображений приватности, он не позволяет собирать достаточно детальные данные. В результате этот API не получится использовать в продакшне для выявления утечек. Рабочая группа W3C Web Performance обсуждала раньше инструменты для работы с памятью, но её членам ещё предстоит договориться о новом стандарте, предназначенном для замены этого API.
Так как прослушиватели событий — это самый распространённый источник утечек памяти, я расскажу ещё об одной используемой мной методике поиска утечек. Она заключается в создании обезьяньих патчей для API addEventListener и removeEventListener и в подсчёте ссылок для проверки того, что их количество возвращается к нулю. Вот пример того, как это делается.
В инструментах разработчика Chrome, кроме того, можно использовать собственный API getEventListeners для того, чтобы выяснить, какие именно прослушиватели событий прикреплены к конкретному элементу. Эта команда, правда, доступна лишь из панели инструментов разработчика.
Хочу добавить, что Матиас Байненс сообщил мне об ещё одном полезном API инструментов Chrome. Это — queryObjects. С его помощью можно получить сведения обо всех объектах, созданных с использованием некоего конструктора. Вот хороший материал на эту тему, посвящённый автоматизации поиска утечек памяти в Puppeteer.
Итоги
Сфера поиска и исправления утечек памяти в веб-приложениях всё ещё находится на заре своего развития. Здесь я рассказал о некоторых методиках, которые, в моём случае, хорошо себя показали. Но следует признать, что применение этих методик всё ещё сопряжено с определёнными трудностями и с большими затратами времени.
Как и в случае с любыми проблемами, касающимися производительности, как говорится, щепотка загодя стоит пуда после. Возможно, кто-то сочтёт полезным подготовить соответствующие синтетические тесты, а не заниматься анализом утечки после того, как она уже произошла. А если речь идёт не об одной утечке, а о нескольких, то анализ проблемы может превратиться в нечто, подобное чистке лука: после того, как исправят одну проблему, обнаруживается другая, а потом этот процесс повторяется (и всё это время, как от лука, слёзы на глазах). Код-ревью тоже могут помочь выявить распространённые паттерны утечек. Но это — в том случае, если знать — куда смотреть.
JavaScript — это язык, который обеспечивает безопасную работу с памятью. Поэтому есть некая ирония в том, как легко в веб-приложениях случаются утечки памяти. Правда, это отчасти так из-за особенностей устройства пользовательских интерфейсов. Нужно прослушивать множество событий: события мыши, прокрутки, клавиатуры. Применение всех этих паттернов легко может привести к появлению утечек памяти. Но, стремясь к тому, чтобы наши веб-приложения экономно расходовали бы память, мы можем повысить их производительность, уберечь их от «падений». Кроме того, мы тем самым демонстрируем уважительное отношение к ограничениям ресурсов устройств пользователей.
Уважаемые читатели! Встречались ли вы с утечками памяти в своих веб-проектах?
Утечки памяти в C++: что это такое и чем они опасны
Разбираемся в трудно уловимых уязвимостях приложений, чтобы всё работало гладко и без тормозов.
Утечка памяти (англ. memory leak) — это неконтролируемое уменьшение свободной оперативной или виртуальной памяти компьютера. Причиной утечек становятся ошибки в программном коде.
В этой статье мы поймём, как появляются утечки и как их устранять.
Как появляются утечки памяти
Любые программы используют в своей работе память, чтобы хранить какие-то данные. В C++ и многих других языках память динамическая. Это значит, что операционная система при запуске программы резервирует какое-то количество ячеек в ОЗУ, а потом выделяет новые, если они нужны.
Создали переменную для числа (int)? Вот тебе 16 бит (4 байта). Нужен массив из ста элементов для больших чисел (long)? Вот тебе ещё 3200 бит (800 байт).
Когда программисту уже не нужен какой-то массив или объект, он должен сказать системе, что его можно удалить с помощью оператора delete[] и освободить память.
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Однако иногда случаются ошибки, которые приводят к утечкам памяти. Вот одна из них:
На примере в цикле десять раз создаётся новый массив, а его адрес записывается в указатель. Адреса старых массивов при этом удаляются. Поэтому дальше оператор delete[] удаляет только последний созданный массив. Остальные останутся в памяти до тех пор, пока не будет закрыта программа.
Чем опасны утечки памяти
Когда приложение съест всю доступную память, сработает защита ОС и ваша программа аварийно закроется. Однако у утечек могут быть и более опасные последствия.
Например, приложение может работать с каким-нибудь файлом непосредственно перед закрытием. В этом случае файл будет повреждён. Последствия возможны самые разные: от нервного срыва пользователя, если это была презентация, над которой он работал несколько дней, до поломки системы, если это был очень важный файл.
В отдельных случаях утечка памяти одного приложения может привести к последствиям для других работающих приложений. Например, если ваш код изменил или занял память, используемую другой программой.
Может показаться, что раз это «утечка», то что-то случится с вашими данными. На самом деле утекает именно свободная память, а не её содержимое.
Как бороться с утечками памяти
Если у вас есть доступ к исходникам, то изучите код, чтобы определить, нет ли там утечек. Вручную делать это бессмысленно, особенно если проект большой, поэтому обратимся к отладчику использования памяти (англ. memory debugger).
Если вы пользуетесь IDE вроде Visual Studio, то там должен быть встроенный отладчик. Есть и сторонние инструменты вроде GDB или LLDB. Отладчик покажет, какие данные хранит программа и к каким ячейкам имеет доступ.