OutOfMemoryError — фатальний виняток, який виникає, коли віртуальна машина Java (JVM) або Android Runtime (ART) не може виділити пам'ять для нового об'єкта через нестачу місця в купі (Heap). За даними Square Engineering, 70% OutOfMemoryError в мобільних додатках викликані витоками пам'яті, а не реальним перевищенням ліміту. Розуміння причин OOM — ключ до стабільної роботи додатка.
Головне
OutOfMemoryError (OOM) — це виняток із сімейства VirtualMachineError в Java/Kotlin, який сигналізує про неможливість виділити пам'ять для нового об'єкта. На відміну від checked-винятків, OOM є Error і не вимагає обробки через catch — хоча технічно його можна перехопити. Після виникнення OOM додаток зазвичай знаходиться в нестабільному стані, і рекомендується завершити його.
На Android кожен додаток має ліміт Heap, встановлений виробником пристрою. Для сучасних смартфонів з 6+ ГБ RAM ліміт становить 256–512 МБ, для бюджетних пристроїв — 128–192 МБ. Коли сумарний обсяг всіх живих об'єктів перевищує цей ліміт, ART викидає OutOfMemoryError.
Важливо розуміти: OOM не завжди означає, що на пристрої закінчилася фізична пам'ять. Це означає, що додаток вичерпав свій ліміт Heap, встановлений системою. Інші додатки можуть мати вільну пам'ять, але ваш додаток не може її використовувати через ізоляцію процесів в Android.
П'ять сценаріїв регулярно призводять до OOM в мобільних додатках. Кожен сценарій пов'язаний з певним типом даних або операцією.
Bitmap — головний споживач пам'яті в Android-додатках. Завантаження FullHD-зображення (1920 × 1080) в оригінальному розмірі займає 8,3 МБ у форматі ARGB_8888. Якщо в RecyclerView 50 таких зображень — це 415 МБ, що перевищує Heap будь-якого пристрою. Завантаження зображень без inSampleSize — гарантований OOM на слабких пристроях.
Використовуйте Glide або Coil для автоматичного масштабування. Ці бібліотеки завантажують зображення з розміром, що відповідає View, а не оригінальному розширенню. Для прямого використання BitmapFactory.Options застосовуйте inSampleSize: розрахуйте його як ступінь двійки, щоб підсумковий розмір не перевищував 2048 × 2048 пікселів. Додатково використовуйте RGB_565 замість ARGB_8888 для зображень без прозорості — це вдвічі зменшує споживання пам'яті.
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
val opts = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
Один витік у кілька КБ не викличе OOM. Але десятки витоків на кожному екрані накопичуються: кожен перехід на екран додає витік, GC не може звільнити об'єкти, Heap заповнюється. Типовий патерн: користувач відкриває та закриває екран профілю 20 разів → Heap зростає на 200 МБ → додаток падає з OOM.
Встановіть LeakCanary в проект для автоматичного виявлення витоків. Він покаже кожен об'єкт, що витік, з точним stack trace. Після виправлення всіх витоків споживання Heap стане стабільним: після закриття екрану пам'ять повертається до базового рівня.
Завантаження файлів цілком у byte[] — прямий шлях до OOM. JSON-файл на 50 МБ при парсингу створить рядок того ж розміру плюс DOM-модель. Відеофайли, завантажені в пам'ять, аудіо-буфери та великі protobuf-датасети — всі вони можуть перевищити ліміт Heap за одну операцію.
Обробляйте великі дані потоками: InputStream з буфером 4–8 КБ, Streaming JSON-парсер (Jackson або Gson з JsonReader), MediaCodec для відео. Ніколи не викликайте File.readBytes() на файлах розміром понад 10% від доступного Heap.
Інтенсивне створення об'єктів у циклі без проміжного GC може призвести до OOM, особливо на пристроях з малим Heap. Приклад: генерація 100 000 об'єктів у for-loop, які не вміщаються в Heap до того, як GC встигне їх зібрати. Це частіше зустрічається в іграх та графічних редакторах.
Використовуйте Object Pool для об'єктів, які створюються та знищуються масово. Для числових даних використовуйте примітиви (FloatArray замість List<Float>). RecyclerView з ViewHolder Pool вирішує цю проблему для UI-компонентів.
Фрагментація — стан, при якому вільної пам'яті сумарно достатньо, але немає безперервного блоку для нового об'єкта. ART компактує Heap при GC, але не завжди успішно. Великі масиви (Bitmap, byte[]) найбільш чутливі до фрагментації.
ART на Android 8+ використовує Generational GC, який знижує фрагментацію за рахунок розділення молодих та старих об'єктів. Тим не менш, уникайте виділення фрагментів різного розміру в одному пулі — намагайтеся використовувати попередньо виділені буфери фіксованого розміру.
Ліміт Heap в Android не є константою — він залежить від виробника, моделі пристрою та версії ОС. Google задає мінімальні вимоги через Compatibility Definition Document (CDD), але виробники встановлюють фактичні значення.
| Категорія пристроїв | Типовий Heap | largeHeap |
|---|---|---|
| Бюджетні (1–2 ГБ RAM) | 128–192 МБ | 256–384 МБ |
| Середні (3–4 ГБ RAM) | 256–384 МБ | 512 МБ |
| Флагмани (6+ ГБ RAM) | 384–512 МБ | 768 МБ–1 ГБ |
| Планшети (4+ ГБ RAM) | 256–512 МБ | 768 МБ |
| Wear OS | 32–64 МБ | Немає |
Запросити збільшений ліміт можна через android:largeHeap="true" в маніфесті. Використовуйте його обережно: збільшення Heap не вирішує проблему витоків і може погіршити користувацький досвід, якщо система буде змушена вбивати інші додатки для звільнення пам'яті для вашого. Для Wear OS ліміт Heap мінімальний — лише 32–64 МБ, тут largeHeap недоступний, і економія пам'яті критична вдвічі.
Діагностика OOM вимагає аналізу Heap Dump та розуміння, які об'єкти споживають пам'ять. Android Studio надає всі необхідні інструменти.
Крок 1: Спіймайте момент OOM. В Android Memory Profiler натисніть Record memory allocations та виконайте сценарій, який викликає падіння. Profiler покаже стрибок алокацій перед OOM. Якщо OOM не відтворюється, зменшіть Heap через android:smallHeap в debug-збірці або використовуйте DDMS з ручним викликом GC.
Крок 2: Зніміть Heap Dump в момент пікового навантаження (до OOM). Відкрийте Dump в Android Studio: вкладка Classes відсортована за Retained Size. Найбільші об'єкти — Bitmap, byte[], String. Для кожного Bitmap подивіться розмір (ширина × висота × 4 байти) та шлях завантаження через Stack Trace.
Крок 3: Аналізуйте кількість повторюваних об'єктів. Якщо ви бачите 200 однакових Fragment або Activity — це витік. Якщо 500 Bitmap з однаковим розміром — проблема кешування зображень. MAT (Memory Analyzer Tool) надає глибший аналіз з Dominator Tree, що показує, які об'єкти утримують 80% Heap.
// Команда для Heap Dump через adb
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.
Комплексна стратегія запобігання OOM включає п'ять рівнів захисту: від архітектурних рішень до моніторингу в продакшені.
ViewModel + Repository патерн відокремлює дані від UI та запобігає утриманню View при повороті екрана. ViewModel переживає Activity, її дані не втрачаються, і View може бути перестворена без дублювання даних в пам'яті. Використовуйте StateFlow замість LiveData для явного керування станами.
Glide — обов'язкова бібліотека для роботи із зображеннями. Вона автоматично масштабує, кешує (диск + пам'ять) та recycle Bitmap. Налаштуйте diskCacheStrategy та skipMemoryCache для великих списків. Для анімованих зображень використовуйте Glide з GIF/WebP — вони займають менше пам'яті, ніж послідовність Bitmap.
Firebase Performance Monitoring відстежує споживання пам'яті в реальному часі. Встановіть алерт на зайняття Heap понад 80% від ліміту — це сигнал для перевірки. Crashlytics збирає OOM як виняток та показує last known Heap state перед падінням. Для Android 11+ використовуйте ApplicationExitInfo для детекції OOM-завершень.
Обов'язково тестуйте додаток на пристроях з мінімальним Heap (128–192 МБ). Емулятор з маленьким екраном та малим Heap емулює бюджетний пристрій. Якщо додаток працює на такому пристрої, на флагманах проблем з OOM не буде. Використовуйте Firebase Test Lab з реальними пристроями різних цінових категорій.
// Перевірка доступного Heap перед важкою операцією
fun canAllocate(requiredBytes: Long): Boolean {
val runtime = Runtime.getRuntime()
val free = runtime.freeMemory()
return free > requiredBytes * 2 // запас 50%
}
Часті запитання
Технічно так, але робити це не рекомендується. Після OOM додаток знаходиться в нестабільному стані: можуть не працювати нові алокації, а деякі об'єкти можуть виявитися частково створеними. Єдина розумна дія в catch — логування та restart Activity.
Ліміт Heap різний на різних пристроях. Операція, яка вимагає 300 МБ, впаде на пристрої з лімітом 192 МБ, але пройде на флагмані з 512 МБ. Тестуйте на пристроях з мінімальними характеристиками для виявлення OOM-сценаріїв.
largeHeap збільшує ліміт, але не прискорює додаток. GC-паузи стають довшими, оскільки збирати великий Heap довше. Система може вбивати фонові додатки для забезпечення пам'яті. Використовуйте largeHeap лише для додатків, яким об'єктивно потрібно багато пам'яті (камери, редактори).
OOM — виняток всередині додатка при нестачі Heap. Системне вбивство (Low Memory Killer) — рішення Linux-ядра вбити процес, щоб звільнити пам'ять для інших додатків. При системному вбивстві додаток не отримує виняток — процес просто завершується.
Формула: ширина × висота × bytesPerPixel. ARGB_8888 = 4 Б/піксель, RGB_565 = 2 Б/піксель. FullHD Bitmap (1920 × 1080) в ARGB_8888 = 8,3 МБ. 4K Bitmap (3840 × 2160) = 33 МБ. Завжди масштабуйте зображення до розміру, необхідного для відображення на екрані.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також