Утечка памяти (Memory Leak) — ситуация, когда приложение удерживает ссылки на объекты, которые больше не нужны, не позволяя сборщику мусора освободить occupied memory. По данным LeakCanary, даже в хорошо написанных приложениях встречается 3–5 утечек на 10 000 строк кода. Каждая утечка постепенно уменьшает доступную память, приводя к торможениям и OutOfMemoryError.
Главное
Утечка памяти (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% случаев в мобильной разработке. Каждая имеет свою причину и характерный паттерн в коде.
Самая известная утечка в Android — хранение статической ссылки на Activity или Context. Типовой код: статическое поле Activity, которое не обнуляется при onDestroy(). Пока живёт статическое поле — живёт весь Activity со своим View-деревом, которое может занимать 1–10 МБ. Это классическая утечка, которую LeakCanary находит в первую очередь.
Решение: никогда не храните Activity или Context в статических полях. Используйте Application Context для синглтонов, которые переживают Activity. Если нужно сослаться на Activity — используйте WeakReference<Activity>.
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() привязан к жизненному циклу.
// автоматическая отписка через Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// корутины с lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
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 имеет два жизненных цикла: самого 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 |
| Perfetto | System-wide трассировка | Временная шкала + native memory |
LeakCanary — must-have для любого Android-проекта. Он автоматически детектирует утечки по окончании жизненного цикла Activity/Fragment и показывает точное место утечки с stack trace. Интеграция: одна строка в build.gradle. LeakCanary 2.x не требует ручной инициализации — он автоматически регистрирует Application Watcher.
Профилактика утечек встраивается в процесс разработки через набор правил и инструментов, проверяющих код на каждом этапе.
Никогда не храните ссылку на Activity, Fragment или View в статическом поле, синглтоне или долгоживущем объекте. Если без ссылки не обойтись — используйте WeakReference или храните данные через ViewModel, которая живёт ровно столько, сколько нужно, и не держит View напрямую.
ViewModel и LiveData из Android Architecture Components решают проблему жизненного цикла на уровне архитектуры. ViewModel переживает поворот экрана и не содержит ссылок на View. LiveData автоматически отписывает observer при onDestroy(). Используйте их вместо ручной подписки на системные сервисы.
На code review обращайте внимание на: статические поля с типами Context/View, анонимные классы, лямбды, замыкающие Activity, ручные подписки, RxJava disposable без composite, хранение Fragment через Bundle. В Kotlin дополнительно проверяйте корутины на launch без привязки к жизненному циклу.
LeakCanary может работать как часть тестового пайплайна: запускайте acceptance-тесты с LeakCanary и fail сборку, если найдена утечка. Это предотвращает попадание утечек в продакшен. Дополните проверку Android Lint с правилом StaticFieldLeak — оно находит потенциальные утечки на уровне статического анализа.
// LeakCanary в тестах
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // fail если есть утечка
}
}
Часто задаваемые вопросы
Утечка — это причина, а OutOfMemoryError — следствие. Одна утечка не приводит к OOM, но накопление десятков утечек исчерпывает Heap. OOM — фатальное исключение, а утечка — это паттерн, который к нему приводит с течением времени.
Через Android Memory Profiler: открывайте и закрывайте экран 5 раз, после каждого закрытия вызывайте GC. Если память не возвращается к базовому уровню — есть утечка. Снимите Heap Dump и найдите в списке класс Activity, количество которого больше 0 после закрытия.
Частично. Kotlin решает проблему null-safety, но не управляет strong references. Корутины с lifecycleScope и viewModelScope предотвращают утечки от фоновых задач, а sealed class и data class уменьшают количество состояний, ведущих к утечкам. Основная защита — архитектурные паттерны, не функции языка.
LeakCanary иногда даёт false positive: объект может быть временно удержан системой (например, InputMethodManager удерживает последнюю View). Проверьте manually: если Retained Size < 1 КБ и GC Root — системный сервис, вероятно, это ложное срабатывание.
Нет. Утечки возможны на любой платформе с GC: iOS (Swift/Objective-C), Flutter (Dart), веб-браузеры (JavaScript). Механизмы одинаковы — strong reference от GC Root. На iOS ARC автоматически управляет памятью, но retain cycle между объектами создаёт ту же утечку.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также