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-модель. Видеофайлы, загруженные в память, audio-буферы и большие 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, который снижает фрагментацию за счёт разделения молодых и старых объектов. Тем не менее, избегайте выделения фрагментов разного размера в одном пуле — старайтесь использовать pre-allocated буферы фиксированного размера.
Лимит 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 посмотрите размер (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 — обязательная библиотека для работы с изображениями. Она автоматически масштабирует, кэширует (диск + память) и 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-ядра убить процесс, чтобы освободить память для других приложений. При системном убийстве приложение не получает исключение — процесс просто завершается.
Формула: width × height × 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также