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+ GB RAM лимитът е 256–512 MB, за бюджетни устройства — 128–192 MB. Когато общият обем на всички живи обекти надвиши този лимит, ART хвърля OutOfMemoryError.
Важно е да се разбере: OOM не винаги означава, че физическата памет на устройството е изчерпана. Това означава, че приложението е изчерпало своя лимит на Heap, зададен от системата. Други приложения може да имат свободна памет, но вашето приложение не може да я използва поради изолацията на процесите в Android.
Пет сценария редовно водят до OOM в мобилните приложения. Всеки сценарий е свързан с определен тип данни или операция.
Bitmap — основният консуматор на памет в Android приложенията. Зареждането на FullHD изображение (1920 × 1080) в оригинален размер заема 8.3 MB във формат ARGB_8888. Ако в RecyclerView има 50 такива изображения — това е 415 MB, което надвишава 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)
}
Едно изтичане от няколко KB няма да причини OOM. Но десетки изтичания на всеки екран се натрупват: всяко преминаване към екран добавя изтичане, GC не може да освободи обектите, Heap се пълни. Типичен модел: потребителят отваря и затваря екрана на профила 20 пъти → Heap расте с 200 MB → приложението пада с OOM.
Инсталирайте LeakCanary в проекта за автоматично откриване на течове. Той ще покаже всеки изтекъл обект с точен stack trace. След коригиране на всички течове, потреблението на Heap става стабилно: след затваряне на екрана паметта се връща към базовото ниво.
Зареждането на файлове изцяло в byte[] — пряк път към OOM. JSON файл от 50 MB при парсиране ще създаде низ със същия размер плюс DOM модел. Видео файлове, заредени в паметта, аудио буфери и големи protobuf набори от данни — всички те могат да надвишат лимита на Heap в една операция.
Обработвайте големи данни с потоци: InputStream с буфер 4–8 KB, Streaming JSON парсер (Jackson или Gson с JsonReader), MediaCodec за видео. Никога не викайте File.readBytes() върху файлове, по-големи от 10% от наличния Heap.
Интензивното създаване на обекти в цикъл без междинен GC може да доведе до OOM, особено на устройства с малък Heap. Пример: генериране на 100 000 обекта в for цикъл, които не се побират в 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 GB RAM) | 128–192 MB | 256–384 MB |
| Средни (3–4 GB RAM) | 256–384 MB | 512 MB |
| Флагмани (6+ GB RAM) | 384–512 MB | 768 MB–1 GB |
| Таблети (4+ GB RAM) | 256–512 MB | 768 MB |
| Wear OS | 32–64 MB | Няма |
Можете да поискате увеличен лимит чрез android:largeHeap="true" в манифеста. Използвайте го внимателно: увеличаването на Heap не решава проблема с течовете и може да влоши потребителското изживяване, ако системата е принудена да убива други приложения за освобождаване на памет. За Wear OS лимитът на Heap е минимален — само 32–64 MB, тук 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 проверете размера (width × height × 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 — задължителна библиотека за работа с изображения. Тя автоматично мащабира, кешира (диск + памет) и рециклира Bitmap. Конфигурирайте diskCacheStrategy и skipMemoryCache за големи списъци. За анимирани изображения използвайте Glide с GIF/WebP — те заемат по-малко памет от последователност от Bitmap-и.
Firebase Performance Monitoring проследява потреблението на памет в реално време. Задайте аларма при заетост на Heap над 80% от лимита — това е сигнал за проверка. Crashlytics събира OOM като изключение и показва последното известно състояние на Heap преди срива. За Android 11+ използвайте ApplicationExitInfo за откриване на OOM завършвания.
Задължително тествайте приложението на устройства с минимален Heap (128–192 MB). Емулатор с малък екран и малък 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 — логване и рестартиране на Activity.
Лимитът на Heap е различен на различните устройства. Операция, която изисква 300 MB, ще падне на устройство с лимит 192 MB, но ще мине на флагман с 512 MB. Тествайте на устройства с минимални характеристики за откриване на OOM сценарии.
largeHeap увеличава лимита, но не ускорява приложението. GC паузите стават по-дълги, тъй като събирането на голям Heap отнема повече време. Системата може да убива фонови приложения за осигуряване на памет. Използвайте largeHeap само за приложения, които обективно се нуждаят от много памет (камери, редактори).
OOM — изключение вътре в приложението при липса на Heap. Системното убиване (Low Memory Killer) — решение на Linux ядрото да убие процес, за да освободи памет за други приложения. При системно убиване приложението не получава изключение — процесът просто приключва.
Формула: width × height × bytesPerPixel. ARGB_8888 = 4 B/пиксел, RGB_565 = 2 B/пиксел. FullHD Bitmap (1920 × 1080) в ARGB_8888 = 8.3 MB. 4K Bitmap (3840 × 2160) = 33 MB. Винаги мащабирайте изображенията до размера, необходим за показване на екрана.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също