Витоку пам’яті в Android що це таке, як виявити та запобігти
У статті ми розповімо, що таке витік пам'яті, як відбувається і які наслідки для операційної системи Android. Також розглянемо інструменти виявлення витоків пам'яті, типові моделі витоку пам'яті в Android, способи оцінки ступеня критичності і методи запобігання основних видів витоків.
Кожному додатку для нормальної роботи потрібна оперативна пам'ять. Для забезпечення необхідною кількістю пам'яті всіх програм Android має ефективно керувати виділенням пам'яті під кожен процес. Середовище виконання Android запускає складання сміття (GC), коли оперативна пам'ять закінчується.
Що таке збирач сміття?
Java Memory Management із вбудованим збирачем сміття є одним із найкращих досягнень цієї мови. Він дозволяє розробникам створювати нові об'єкти, не переймаючись розподілом пам'яті та її звільнення, оскільки збирач сміття автоматично відновлює пам'ять для повторного її використання. Це забезпечує більш швидку розробку з меншою кількістю коду, одночасно усуваючи витік пам'яті та інші проблеми, пов'язані з нею. Принаймні теоретично.
За іронією долі збирач сміття Java працює надто добре, створюючи та видаляючи велику кількість об'єктів. Більшість проблем керування пам'яттю вирішуються, але часто за рахунок зменшення продуктивності. Створення універсального збирача сміття, що застосовується до всіх можливих ситуацій, спричинило труднощі з оптимізацією системи. Щоб розібратися зі збирачем сміття, потрібно спочатку зрозуміти, як працює керування пам'яттю на віртуальній машині Java (JVM).
Як працює збирач сміття
Багато хто вважає, що збирач сміття збирає та видаляє з пам'ятіоб'єкти, що не використовуються. Насправді збирач сміття Java робить усе навпаки. Живі об'єкти відзначаються як активні, проте інше вважається сміттям. Як наслідок, ця фундаментальна особливість може призвести до багатьох проблем із продуктивністю.
Почнемо з так званої купи («heap») — області пам'яті, що використовується для динамічного розподілу ресурсів додатків. У більшості конфігурацій операційна система заздалегідь віддає цю частину під управління JVM під час роботи програми. Це призводить до наслідків:
- Створення об'єкта відбувається швидше, тому що глобальна синхронізація з операційною системою не потрібна для кожного окремого об'єкта. У процесі виділення пам'яті під програму JVM просто фіксує за завданням певну ділянку пам'яті і переміщує покажчик зміщення вперед (зображення нижче). Наступний розподіл починається з цього усунення і займає наступну ділянку пам'яті;
- коли об'єкт більше не використовується, збирач сміття відновлює базовий стан цієї ділянки пам'яті та повторно використовує її для розміщення іншого об'єкта. Це означає, що немає явного видалення, і пам'ять все ще не буде очищена.
Нові об'єкти просто розміщуються наприкінці купи.
Коріння збирача сміття - початкова позиція всіх ієрархій (дерев) об'єктів
Кожне дерево об'єктів повинно мати один або кілька кореневих об'єктів. Поки програма може досягти цього коріння, все дерево доступне. Але коли ці кореневі об'єкти вважаються доступними? Спеціальні об'єкти, які називають корінням збирача сміття (коріння GC, малюнок нижче), завжди доступні, а також будь-який об'єкт, чиїм коренем є корінь збирача сміття.
У Java існують такі типи коренів збирача сміття:
Коріння збирача сміттяце об'єкти, які посилаються на 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 і знайти дію, яка має бути відключена. Перевіряємо дані TotalCount , і якщо знайшли відмінності в даних, значить десь є витік пам'яті.
- Як тільки було виявлено витік, потрібно перевірити дерево посилань і дізнатися, який об'єкт його викликає.
Які загальні схеми витоків?
- витоку пам'яті, що ініціюються статичним посиланням;
- витоку пам'яті, ініційовані робочим процесом;
- просто витік.
Можна завантажити програму SinsOfMemoryLeaks, яка допоможе визначити, де відбувається витік.
У гілці Leak буде видно причини витоку пам'яті. Цю програму можна також запустити на пристрої або емуляторі та використовувати вищезазначені інструменти для відстеження витоків. У гілці FIXED можна побачити поради, як виправити витік. Після виправлення процедуру можна повторити заново, щоб остаточно переконатися, що витоку виправлено. Кожна з гілок програми має різні ідентифікатори програм, тому можна встановити їх на одному пристрої і перевіряти показання одночасно.
А тепер швидко пройдемося з усіх видів витоків.
Витоку пам'яті, ініційовані статичним посиланням
Витоку пам'яті, що ініціює робочий процес
Той самий принцип застосовується до таких потоків, як thread pool або ExecutorService.
Просто витік
Щоразу під час запуску робочого потоку з операції ви самі відповідаєте за керування потоком. Оскільки робочий потік може працювати довше за саму операцію, потрібно зупинити його, коли дія буде припинена. Якщо цього не зробити, існує можливість витоку пам'яті робочого процесу. Як у цьому репозиторії.
Яким є вплив конкретного витоку?
В ідеалі слід уникати написання коду, який може спричинити витік пам'яті, і виправити всі витоки, що існують у додатку. Але насправді, якщоНеобхідно працювати зі старою базою коду і визначити пріоритети завдань, включаючи виправлення витоків пам'яті, можна оцінити рівень серйозності в наступних аспектах.
Наскільки великий витік пам'яті?
Не всі витоки пам'яті однакові. Деякі витоку можуть становити кілька кілобайт, а деякі кілька мегабайт. Це можна визначити, використовуючи інструменти представлені вище і вирішити, чи має розмір пам'яті критичне значення для пристроїв користувача.
Як довго триває витік?
Деякі витоку через робочий потік живуть до тих пір, поки цей потік працює. У такому разі потрібно вивчити, наскільки довго живе цей потік. У прикладі програми вище створені нескінченні цикли в робочому потоці, тому вони постійно тримають у пам'яті об'єкт, що породжує витік. Але насправді більшість робочих потоків виконує прості завдання, такі як доступ до файлової системи або виконання мережевих викликів, які або недовговічні, або обмежені тайм-аутом.
Скільки об'єктів у витоку?
У деяких випадках витік породжує лише один об'єкт, наприклад, один із прикладів статичних посилань, показаний у додатку SinsOfMemoryLeaks. Щойно буде створено нову дію, вона почне посилатися на нову операцію. Старий витік буде очищений збирачем сміття. Таким чином, максимальний витік завжди дорівнює розміру одного екземпляра операції. Однак інші витоки продовжують просочуватися в нові об'єкти в міру їхнього створення. У прикладі Leaking Threads активність пропускає по одному потоку щоразу під час його створення. Тому, якщо ви повертаєте пристрій 20 разів, витік становитиме 20 робочих потоків. Це закінчиться дуже сумно, оскільки програма заповнить усю доступну пам'ять на пристрої.
Як виправити тазапобігти витоку
Подивіться, як відбувається усунення типових витоків пам'яті в цій гілці репозиторію. Рішення можна узагальнити до таких пунктів:
Не забудьте перевірити приклади коду для типових витоків пам'яті та способи їх уникнення в репозиторії Github.