Что означает ошибка OutOfMemoryError
OutOfMemoryError (часто сокращенно OOM) — это критическая ошибка времени выполнения в Java и Android, которая возникает, когда виртуальная машина (ART на Android) не может выделить объект в куче (heap) из-за нехватки памяти. Ошибка обычно выглядит так:
java.lang.OutOfMemoryError: Failed to allocate a 123456-byte allocation with 12345 free bytes and 12MB until OOM
Она приводит к немедленному падению приложения. В Android эта ошибка особенно распространена на устройствах с ограниченной памятью (например, бюджетные модели) или при работе с большими ресурсами (изображения, JSON-ответы, кэши).
Причины возникновения
OutOfMemoryError не случается случайно. Вот наиболее частые причины:
- Утечки памяти (memory leaks) — объекты, которые больше не нужны, но продолжают удерживаться в памяти (например, через статические ссылки, незакрытые слушатели, контекст активности в фоновых задачах).
- Загрузка слишком больших изображений — без должного масштабирования или сжатия, особенно при использовании
BitmapFactory.decode*без указанияinSampleSize. - Слишком большой размер кучи (heap) — приложение пытается создать объект, который физически не помещается в доступную память (например, огромный массив или список).
- Неэффективные структуры данных — использование
HashMapилиArrayListбез ограничения роста, хранение дублирующих данных. - Неосвобождаемые ресурсы — незакрытые
Cursor,InputStream,OutputStream,Socket, которые держат память и файловые дескрипторы. - Избыточное кэширование — хранение в кэше (например,
LruCache) слишком много объектов или объектов большого размера. - Работа с большими JSON/XML — парсинг огромных строк в объекты без потокового подхода.
Способы решения
Способ 1: Анализ памяти и обнаружение утечек
Первым делом нужно понять, что именно потребляет память. Android Studio предоставляет мощный инструмент — Memory Profiler.
- Откройте проект в Android Studio.
- Запустите приложение на устройстве или эмуляторе.
- В меню выберите View → Tool Windows → Profiler (или нажмите
Alt+Shift+F10). - В окне Profiler выберите ваше устройство и приложение, затем перейдите на вкладку Memory.
- Нажмите кнопку Record (кружок) и выполните действия, которые обычно приводят к падению (например, открыть экран с изображениями, прокрутить список).
- После появления роста памяти нажмите Pause, затем Dump Java heap (значок камеры). Откроется окно с анализом кучи.
- В левой панели найдите подозрительные классы (например, ваши Activity, Fragment, Bitmap) и проверьте количество экземпляров. Если объекты не освобождаются после выхода из экрана — это утечка.
- Для детального анализа используйте Analyzer Tasks → Find Leaks. Профилировщик покажет цепочки ссылок, удерживающие объекты.
💡 Совет: Обратите внимание на
ActivityиContext— самые частые виновники утечек. Если в статическом поле или в долгоживущем объекте (например, синглтоне) хранится ссылка наActivity, она не будет собрана сборщиком мусора.
Способ 2: Оптимизация работы с изображениями
Изображения — самая частая причина OOM. Даже небольшое Bitmap в памяти занимает в 4-8 раз больше места, чем на диске (из-за ARGB_8888).
Используйте библиотеки для загрузки изображений (Glide, Picasso, Coil). Они автоматически:
- Масштабируют изображение под размер
ImageView. - Кэшируют в памяти и на диске.
- Управляют жизненным циклом (отменяют загрузку при уничтожении Activity).
Пример с Glide:
// build.gradle (Module)
dependencies {
implementation 'com.github.bumptech.glide:glide:4.15.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
}
// Загрузка с автоматическим масштабированием
Glide.with(context)
.load(imageUrl)
.override(targetWidth, targetHeight) // опционально: явное указание размера
.into(imageView)
Если библиотеки не используются:
- Всегда указывайте
inSampleSizeпри декодировании:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
// Вычисляем inSampleSize на основе требуемых размеров
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
- Используйте
Bitmap.Config.RGB_565(2 байта на пиксель вместо 4), если альфа-канал не нужен. - Явно вызывайте
bitmap.recycle()(для API < 28) или присваивайтеnullи вызывайтеSystem.gc()(не гарантирует, но иногда помогает).
Способ 3: Правильное управление ресурсами и жизненным циклом
Многие утечки возникают из-за того, что ресурсы не закрываются, или фоновые задачи держат ссылку на Activity.
- Закрывайте все ресурсы в
finally-блоках или используйте try-with-resources (Java 7+):
try (Cursor cursor = db.rawQuery(...);
InputStream is = ...) {
// работа
} // автоматически закроется
- Отменяйте фоновые задачи в
onDestroy()илиonStop():
override fun onDestroy() {
super.onDestroy()
job.cancel() // для корутин
call.cancel() // для Retrofit
handler.removeCallbacksAndMessages(null)
}
- Избегайте неявных ссылок на Context:
- Не храните
Contextв статических полях. - Используйте
applicationContextтам, где не нужен UI-контекст. - В фрагментах и активностях используйте
viewLifecycleOwnerдля жизненного цикла.
- Используйте слабые ссылки (WeakReference) для кэшей, которые могут содержать контекст.
Способ 4: Увеличение размера кучи (временное решение)
Если оптимизации не помогают, а приложение критически важно, можно временно увеличить лимит памяти.
В AndroidManifest.xml в теге <application> добавьте:
<application
android:largeHeap="true"
... >
⚠️ Важно: Это не панацея.
largeHeapувеличивает лимит только на некоторых устройствах (обычно в 2-3 раза), но не снимает проблему утечек. Используйте только как временную меру, пока не исправите коренную причину. На новых версиях Android (особенно с ограничениями в фоновом режиме) этот атрибут может игнорироваться.
Профилактика
Чтобы избежать OutOfMemoryError в будущем:
- Регулярно профилируйте память — добавьте Memory Profiler в процесс разработки, особенно перед releasing.
- Используйте статический анализ — инструменты вроде Android Lint и SonarQube могут выявить потенциальные утечки.
- Внедрите LeakCanary — библиотека автоматически обнаруживает утечки в debug-сборках:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
- Ограничивайте размеры коллекций — используйте
LruCacheдля кэширования, задавайте лимиты спискам. - Обрабатывайте большие данные потоково — не загружайте весь JSON/файл в память, используйте
JsonReaderилиXmlPullParser. - Тестируйте на устройствах с малым объемом RAM — emulateur с 512MB или реальные бюджетные телефоны.
Способ N: Использование статических анализаторов и CI
Для сложных проектов автоматизируйте поиск утечек:
- Интегрируйте LeakCanary в CI (например, с Firebase Test Lab) — запускайте тесты на устройствах и проверяйте отчеты.
- Используйте Perfetto — системный трейсер Android, который может выявить проблемы с памятью на уровне системы.
- Проводите регулярный аудит кода на предмет:
- Статических ссылок на
Context/View. - Незакрытых ресурсов в
finally. - Регистрации слушателей без соответствующих
unregister.
- Статических ссылок на
Профилактика (дополнительно)
Помимо вышеперечисленного, следите за:
- Размером APK — большие ресурсы (未 сжатые изображения, аудио) могут привести к OOM при первом запуске.
- Количеством процессов — на устройствах с Android 8+ ограничено количество фоновых процессов. Избыточные сервисы могут привести к нехватке памяти.
- Адаптацией под разные плотности экрана — загружайте только нужные ресурсы (
drawable-hdpi,xhdpiи т.д.) черезResources.getIdentifier()или CDN.
Заключительный совет: Начните с профилирования — 90% проблем OOM находятся в первые 10 минут анализа Memory Profiler. Не игнорируйте предупреждения Lint о утечках, они часто точечны.