Утечка памяти в мобильных приложениях — что это такое, причины и методы поиска

Автор: IT Sectr Опубликовано: 2026-03-29 Время чтения: 9 мин

Утечка памяти (Memory Leak) — ситуация, когда приложение удерживает ссылки на объекты, которые больше не нужны, не позволяя сборщику мусора освободить occupied memory. По данным LeakCanary, даже в хорошо написанных приложениях встречается 3–5 утечек на 10 000 строк кода. Каждая утечка постепенно уменьшает доступную память, приводя к торможениям и OutOfMemoryError.

Главное

  • Memory Leak — объект остаётся в памяти, хотя на него нет активных ссылок из логики приложения
  • Статические ссылки на Activity или Context — самая частая причина утечек в Android
  • LeakCanary — стандартный инструмент для автоматического обнаружения утечек в Android
  • WeakReference и Application Context — базовые техники предотвращения утечек
  • Lifecycle-aware компоненты устраняют целый класс утечек, связанных с подписками

Что такое утечка памяти

Утечка памяти (Memory Leak) — это ситуация, в которой объект остаётся достижимым через цепочку сильных ссылок (Strong Reference), хотя логически уже не нужен приложению. Сборщик мусора (GC) считает такой объект живым и не освобождает занимаемую им память. В результате доступная память кучи (Heap) постоянно уменьшается, а частота GC-пауз растёт.

В отличие от языков с ручным управлением памятью (C, C++), в Java/Kotlin утечка — это не забытый free(), а забытая ссылка. Пока существует strong reference от корневого объекта (GC Root) до утекшего объекта, GC считает его нужным. Типовые GC Root: статические поля, активные потоки, стек вызовов, JNI-глобальные ссылки.

Опасность утечек — их накопительный эффект. Одна утечка в 100 КБ не заметна, но 100 таких утечек занимают 10 МБ, и приложение начинает тормозить из-за частых GC. Критическая масса утечек приводит к OutOfMemoryError и крашу приложения. Симптомы утечки: постоянный рост потребления памяти на графике Profiler, частые GC-паузы с STW (Stop The World) и падение производительности UI.

Типичные виды утечек в мобильных приложениях

Пять видов утечек покрывают 95% случаев в мобильной разработке. Каждая имеет свою причину и характерный паттерн в коде.

Статическая ссылка на Activity или Context

Самая известная утечка в Android — хранение статической ссылки на Activity или Context. Типовой код: статическое поле Activity, которое не обнуляется при onDestroy(). Пока живёт статическое поле — живёт весь Activity со своим View-деревом, которое может занимать 1–10 МБ. Это классическая утечка, которую LeakCanary находит в первую очередь.

Решение: никогда не храните Activity или Context в статических полях. Используйте Application Context для синглтонов, которые переживают Activity. Если нужно сослаться на Activity — используйте WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Внутренние классы с неявной ссылкой

Анонимные классы и не-статические вложенные классы неявно хранят ссылку на containing class. Runnable, передаваемый в Handler, который выполняется после onDestroy(), удерживает весь Activity. Callback Retrofit, замыкающий Activity, делает то же самое. Это самый коварный тип утечки — неявная ссылка не видна в коде.

Kotlin object-выражения и лямбды тоже захватывают ссылки на внешний класс. Делайте вложенные классы статическими (или top-level в Kotlin) и передавайте внешние ссылки через WeakReference. Для лямбд используйте Lifecycle-aware подход с viewLifecycleOwner.

Неотписанные слушатели и подписки

Подписка на системные сервисы без отписки — прямая утечка. SensorManager, LocationManager, NotificationListener, зарегистрированные в onResume() без вызова unregister в onPause(), удерживают Activity. Аналогично: RxJava Disposable, не добавленный в CompositeDisposable, и корутина, запущенная через GlobalScope.

Используйте Lifecycle-aware компоненты: observe() с LifecycleOwner автоматически отписывается при onDestroy(). Для RxJava — viewLifecycleOwner.lifecycle.addObserver с DisposableObserver. Для корутин — lifecycleScope.launch() привязан к жизненному циклу.

kotlin
// автоматическая отписка через Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// корутины с lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap без recycle

Bitmap занимает значительный объём памяти кучи: один FullHD-бит — 1920 × 1080 × 4 байта = 8.3 МБ. Если Bitmap создаётся для каждого элемента списка и не вызывается recycle() при скрытии, память быстро исчерпывается. На старых версиях Android (до 3.0) Bitmap хранился в native-памяти, но на современных — в куче Dalvik/ART, и GC может освободить его только если нет strong reference.

Используйте Glide или Coil для загрузки изображений — эти библиотеки управляют кэшированием и recycle автоматически. Если работаете с Bitmap напрямую, вызывайте bitmap.recycle() для больших изображений, которые больше не отображаются, и используйте inSampleSize для загрузки уменьшенных копий.

Fragment Reference после onDestroyView

Fragment имеет два жизненных цикла: самого Fragment и его View. После onDestroyView() View-дерево уничтожается, но сам Fragment может оставаться в памяти, если есть внешняя ссылка. Типовая ошибка — хранение ссылки на Fragment в адаптере ViewPager или в navigation graph, которая не очищается при уничтожении.

Никогда не храните ссылку на Fragment в полях долгоживущих объектов. Используйте childFragmentManager для вложенных фрагментов и observe() с LifecycleOwner для передачи данных между ними. ViewPager2 решил эту проблему на уровне API: FragmentTransactionAdapter корректно управляет жизненным циклом.

Как обнаружить утечку памяти

Обнаружение утечки требует проверки двух фактов: память не возвращается после expected lifetime и количество объектов определённого типа растёт без уменьшения. Процесс диагностики включает три этапа.

Первый этап — визуальная проверка через Memory Profiler в Android Studio. Откройте вкладку Memory, выполните целевое действие (откройте и закройте экран), нажмите GC (Garbage Collection) и смотрите, вернулась ли память к исходному уровню. Если после 3–4 циклов открытия-закрытия память стабильно растёт — есть утечка.

Второй этап — снятие Heap Dump. В Memory Profiler нажмите Dump Java Heap. Полученный .hprof-файл откройте в Android Studio: вы увидите все объекты в куче с размерами и ссылками. Ищите классы, количество которых должно быть нулевым после закрытия экрана. Например, MainActivity с количеством 2 после закрытия — явная утечка.

Третий этап — анализ Retained Size и GC Root. В Android Studio анализируйте Retained Size: сколько памяти освободится, если удалить этот объект. Путь от GC Root до объекта показывает, что его держит: Static field → HashMap → Activity — и вы видите точку утечки. Панель Reference виджет показывает всех держателей объекта.

Инструменты для поиска утечек

Четыре инструмента покрывают поиск утечек от автоматического обнаружения до глубинного анализа Heap Dump.

ИнструментМетодФормат результатов
LeakCanaryАвтоматический мониторингHeap Dump + stack trace утечки
Android Memory ProfilerРучной мониторингГрафик памяти + Heap Dump
MAT (Eclipse)Глубинный анализОтчёт Dominator Tree + GC Root path
PerfettoSystem-wide трассировкаВременная шкала + native memory

LeakCanary — must-have для любого Android-проекта. Он автоматически детектирует утечки по окончании жизненного цикла Activity/Fragment и показывает точное место утечки с stack trace. Интеграция: одна строка в build.gradle. LeakCanary 2.x не требует ручной инициализации — он автоматически регистрирует Application Watcher.

Как предотвратить утечки памяти

Профилактика утечек встраивается в процесс разработки через набор правил и инструментов, проверяющих код на каждом этапе.

Правило сильных ссылок

Никогда не храните ссылку на Activity, Fragment или View в статическом поле, синглтоне или долгоживущем объекте. Если без ссылки не обойтись — используйте WeakReference или храните данные через ViewModel, которая живёт ровно столько, сколько нужно, и не держит View напрямую.

Lifecycle-aware архитектура

ViewModel и LiveData из Android Architecture Components решают проблему жизненного цикла на уровне архитектуры. ViewModel переживает поворот экрана и не содержит ссылок на View. LiveData автоматически отписывает observer при onDestroy(). Используйте их вместо ручной подписки на системные сервисы.

Code Review с фокусом на GC Root

На code review обращайте внимание на: статические поля с типами Context/View, анонимные классы, лямбды, замыкающие Activity, ручные подписки, RxJava disposable без composite, хранение Fragment через Bundle. В Kotlin дополнительно проверяйте корутины на launch без привязки к жизненному циклу.

Автоматическая проверка в CI

LeakCanary может работать как часть тестового пайплайна: запускайте acceptance-тесты с LeakCanary и fail сборку, если найдена утечка. Это предотвращает попадание утечек в продакшен. Дополните проверку Android Lint с правилом StaticFieldLeak — оно находит потенциальные утечки на уровне статического анализа.

kotlin
// LeakCanary в тестах
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fail если есть утечка
    }
}

Часто задаваемые вопросы

Чем утечка памяти отличается от OutOfMemoryError?

Утечка — это причина, а OutOfMemoryError — следствие. Одна утечка не приводит к OOM, но накопление десятков утечек исчерпывает Heap. OOM — фатальное исключение, а утечка — это паттерн, который к нему приводит с течением времени.

Как найти утечку без LeakCanary?

Через Android Memory Profiler: открывайте и закрывайте экран 5 раз, после каждого закрытия вызывайте GC. Если память не возвращается к базовому уровню — есть утечка. Снимите Heap Dump и найдите в списке класс Activity, количество которого больше 0 после закрытия.

Может ли Kotlin предотвратить утечки на уровне языка?

Частично. Kotlin решает проблему null-safety, но не управляет strong references. Корутины с lifecycleScope и viewModelScope предотвращают утечки от фоновых задач, а sealed class и data class уменьшают количество состояний, ведущих к утечкам. Основная защита — архитектурные паттерны, не функции языка.

Почему LeakCanary находит утечку, которой нет?

LeakCanary иногда даёт false positive: объект может быть временно удержан системой (например, InputMethodManager удерживает последнюю View). Проверьте manually: если Retained Size < 1 КБ и GC Root — системный сервис, вероятно, это ложное срабатывание.

Утечки памяти есть только на Android?

Нет. Утечки возможны на любой платформе с GC: iOS (Swift/Objective-C), Flutter (Dart), веб-браузеры (JavaScript). Механизмы одинаковы — strong reference от GC Root. На iOS ARC автоматически управляет памятью, но retain cycle между объектами создаёт ту же утечку.

Итоги

  • Memory Leak — объект, который GC не может освободить из-за забытой strong reference
  • Статические ссылки на Activity и Context — самая распространённая причина утечек
  • Невные ссылки через анонимные классы, лямбды и RxJava подписки коварнее явных
  • LeakCanary автоматически находит утечки и показывает точный stack trace
  • Lifecycle-aware компоненты (ViewModel, LiveData, lifecycleScope) устраняют класс утечек
  • Heap Dump и анализ Retained Size — основной метод ручной диагностики
  • Профилактика включает code review на strong reference и CI-проверку LeakCanary

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также