Утечка памяти (memory leak) — ситуация, когда приложение не освобождает память, занятую объектами, которые больше не нужны. В мобильной разработке это особенно критично: ограниченный heap и отсутствие swap приводят к OutOfMemoryError и падению приложения. По данным Purdue University (2022), 35% Android-приложений в Google Play содержат хотя бы одну утечку памяти. Разберём типичные сценарии, инструменты диагностики и методы устранения.
Главное
Утечка памяти — это ситуация, когда выделенная память не возвращается системе после того, как объект перестал быть нужен программе. Сборщик мусора считает такой объект живым, потому что на него ведёт активная цепочка ссылок от GC Root.
В Java/Kotlin сборщик мусора работает автоматически, но он не может определить, что объект логически не нужен, если на него есть техническая ссылка. Разработчик должен явно обрывать ненужные связи. В Swift/Objective-C ARC автоматически подсчитывает ссылки, но retain cycles блокируют обнуление счётчика.
Основная опасность утечек — кумулятивный эффект. Каждая утечка съедает небольшой объём памяти, но при многократных переходах между экранами (поворотах экрана, открытии/закрытии Activity) утечки накапливаются, пока не исчерпают лимит heap.
Утечка — объект недоступен коду, но не удалён GC. Раздувание — объект логически нужен, но хранится в избыточном количестве. Пример раздувания: кеш изображений на 100 MB при рабочем наборе в 30 MB. Обе проблемы ведут к OOM, но причины и методы лечения разные.
ART (Android Runtime) использует поколенческую сборку мусора с concurrent compaction. Память делится на молодое поколение (Young), старое (Old) и огромные объекты (Large). Объекты, пережившие несколько циклов GC, перемещаются в Old generation, где сборка происходит реже — это ускоряет обычные циклы.
GC начинается, когда heap достигает определённого порога занятости (обычно 75-85%). Во время GC все потоки приложения приостанавливаются (STW — Stop The World). Чем больше живых объектов, тем дольше пауза. Утечки увеличивают количество живых объектов, удлиняя GC паузы.
Сборщик определяет живые объекты, обходя граф от GC Roots: статические поля, стековые переменные активных потоков, JNI references. Любой объект, достижимый по ссылкам от этих корней, считается живым — даже если разработчик знает, что он больше не нужен.
// Example: static collection as GC Root — permanent leak
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference does not prevent GC — correct behavior
}
}
WeakReference решает проблему: GC игнорирует weak ссылки при определении живых объектов. Если на объект остались только weak ссылки, он будет собран в ближайший цикл GC.
Activity Context — самый массовый сценарий утечек в Android. Если синглтон, статическое поле или долгоживущий сервис хранит ссылку на Activity Context, то вся Activity со всеми View не может быть собрана GC. Решение: используйте Application Context для долгоживущих объектов.
Handler и посланные сообщения — Handler.postDelayed(runnable, delay) ставит сообщение в очередь Main Looper. Если Activity уничтожена до истечения delay, сообщение всё ещё в очереди и держит ссылку через Runnable → анонимный класс → внешний класс (Activity).
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // mandatory: clear queue
super.onPause()
}
}
Inner Classes — нестатический внутренний класс имеет неявную ссылку на экземпляр внешнего класса. Если внешний класс — Activity, а внутренний класс передан куда-то вовне (например, в RecyclerView.Adapter), Activity не может быть собрана.
Android Studio Memory Profiler — встроенный инструмент для мониторинга heap в реальном времени. Показывает график занятой памяти, количество аллокаций и объектов по типам. Позволяет записать heap dump и экспортировать в HPROF-формат для анализа в MAT.
Eclipse MAT (Memory Analyzer Tool) — десктопный анализатор heap dump. Автоматически строит Leak Suspects Report, который подсвечивает объекты с наибольшим retained size и предлагает предполагаемую GC root chain для каждого подозрительного объекта.
Xcode Memory Graph Debugger — для iOS. Приостанавливает приложение и визуализирует граф объектов. Retain cycles подсвечиваются красным, можно кликнуть на любой объект и увидеть его retain count и ссылки.
| Инструмент | Возможности | Сложность |
|---|---|---|
| Memory Profiler | Real-time график, heap dump, Object Allocation Tracking | Низкая |
| Eclipse MAT | Dominator tree, Leak Suspects, OQL запросы | Средняя |
| LeakCanary | Автоматическое обнаружение, trace утечки в уведомлении | Минимальная |
| Xcode Memory Graph | Визуальный граф retain cycles, live object list | Низкая |
По данным Uber Engineering Blog, внедрение автоматического профилирования памяти (LeakCanary + heap dump analysis) в CI/CD пайплайн сокращает количество memory-related инцидентов в production на 60% в течение 3 месяцев.
Замена Context — если объект живёт дольше Activity, используйте applicationContext. Все долгоживущие объекты (синглтоны, репозитории, database helpers) должны получать Application Context, а не Activity Context. Исключение: UI-компоненты, которым нужен доступ к теме или ресурсам, специфичным для Activity.
Lifecycle-aware компоненты — использование LifecycleObserver, DefaultLifecycleObserver или reactivelx автоматически отменяет подписки при onDestroy. Android Jetpack предоставляет lifecycleScope и viewModelScope, которые очищаются соответствующим событием.
Static inner class — если внутреннему классу не нужен доступ к полям внешнего, сделайте его static. Статический внутренний класс не имеет неявной ссылки на внешний. Если доступ нужен, используйте WeakReference для явной ссылки.
class MyActivity : AppCompatActivity() {
// ❌ Non-static inner class — implicit reference to MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Static inner class — no implicit reference
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
В iOS используйте capture lists: [weak self] в замыканиях, которые могут пережить создателя. Для делегатов используйте слабые ссылки (weak var delegate). Для замыканий, которые гарантированно вызываются только при жизни self, можно использовать [unowned self], но осторожно — при обращении к освобождённому объекту будет crash.
Часто задаваемые вопросы
В Android выполните несколько переходов между экранами (Activity A → B → A → B) и проверьте adb shell dumpsys meminfo package_name. Если Total PSS стабильно растёт и не возвращается к исходному значению — есть утечка. В iOS аналогично: используйте Debug Memory Graph в Xcode для визуальной проверки.
Да, если CoroutineScope не отменён при уничтожении компонента. Корутина, запущенная в GlobalScope, продолжает выполняться даже после finish() Activity. Решение: используйте viewModelScope (отменяется в onCleared) или lifecycleScope (отменяется в onDestroy). Для собственных Scope создавайте lifecycle-aware scopes через LifecycleOwner.
Bitmap хранит пиксельные данные в native heap, а не в Java heap. Это означает, что Java GC не видит реального размера Bitmap. Если Bitmap не вызвать recycle() или не обнулить ссылку, native память не освободится. Используйте BitmapFactory с inSampleSize для загрузки уменьшенных копий и Glide/Coil для автоматического управления кешем.
Статическое поле — это GC Root. Оно живёт, пока загружен класс (в Android — пока жив Process). Если статическое поле ссылается на Activity, Bitmap, View или любой другой тяжёлый объект, этот объект никогда не будет собран GC. Статическое поле — вечная ссылка. Решение: храните только WeakReference или обнуляйте static поле в onDestroy.
ARC автоматически освобождает объекты, когда счётчик сильных ссылок падает до нуля. Retain cycle — единственный способ утечки при ARC. Всегда используйте weak для ссылок parent→child, где child должен пережить parent (делегаты, data source). Для замыканий используйте capture list [weak self] и проверяйте self на nil внутри замыкания.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также