Течёт память: что это, типичные сценарии и диагностика

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

Утечка памяти (memory leak) — ситуация, когда приложение не освобождает память, занятую объектами, которые больше не нужны. В мобильной разработке это особенно критично: ограниченный heap и отсутствие swap приводят к OutOfMemoryError и падению приложения. По данным Purdue University (2022), 35% Android-приложений в Google Play содержат хотя бы одну утечку памяти. Разберём типичные сценарии, инструменты диагностики и методы устранения.

Главное

  • GC Root — точка входа, через которую сборщик мусора определяет живые объекты
  • Context утечка — передача Activity Context в синглтон приводит к удержанию всей View hierarchy
  • Handler с postDelayed — если Activity уничтожена, Handler не даёт ей уйти на GC
  • Heap dump — основной метод анализа утечек через MAT или Android Profiler
  • SoftReference — альтернатива WeakReference для кешей с автоматической очисткой при нехватке памяти

Что такое утечка памяти в мобильных приложениях?

Утечка памяти — это ситуация, когда выделенная память не возвращается системе после того, как объект перестал быть нужен программе. Сборщик мусора считает такой объект живым, потому что на него ведёт активная цепочка ссылок от 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. Любой объект, достижимый по ссылкам от этих корней, считается живым — даже если разработчик знает, что он больше не нужен.

kotlin
// 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.

Типичные сценарии утечек в Android и iOS

Activity Context — самый массовый сценарий утечек в Android. Если синглтон, статическое поле или долгоживущий сервис хранит ссылку на Activity Context, то вся Activity со всеми View не может быть собрана GC. Решение: используйте Application Context для долгоживущих объектов.

Handler и посланные сообщения — Handler.postDelayed(runnable, delay) ставит сообщение в очередь Main Looper. Если Activity уничтожена до истечения delay, сообщение всё ещё в очереди и держит ссылку через Runnable → анонимный класс → внешний класс (Activity).

kotlin
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 не может быть собрана.

  • TimerTask и ScheduledExecutorService — задачи, запланированные до уничтожения Activity
  • BroadcastReceiver — незарегистрированный в onPause/onDestroy продолжает держать Context
  • ViewModel с ссылкой на View — ViewModel переживает Activity, ссылка на View ведёт к утечке
  • Retrofit Call — если Call не отменён, ответ приходит в уничтоженный Fragment

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

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 ProfilerReal-time график, heap dump, Object Allocation TrackingНизкая
Eclipse MATDominator 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 для явной ссылки.

kotlin
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 для визуальной проверки.

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

Да, если CoroutineScope не отменён при уничтожении компонента. Корутина, запущенная в GlobalScope, продолжает выполняться даже после finish() Activity. Решение: используйте viewModelScope (отменяется в onCleared) или lifecycleScope (отменяется в onDestroy). Для собственных Scope создавайте lifecycle-aware scopes через LifecycleOwner.

Как Bitmap влияет на утечки?

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.

Как избежать утечек в iOS с ARC?

ARC автоматически освобождает объекты, когда счётчик сильных ссылок падает до нуля. Retain cycle — единственный способ утечки при ARC. Всегда используйте weak для ссылок parent→child, где child должен пережить parent (делегаты, data source). Для замыканий используйте capture list [weak self] и проверяйте self на nil внутри замыкания.

Итоги

  • Утечка памяти — объект недоступен коду, но не удалён GC, потому что на него есть активная ссылка от GC Root
  • GC Roots включают статические поля, стековые переменные и JNI references; любой объект, достижимый от них, жив
  • Context утечка — самая массовая проблема в Android: передача Activity Context в синглтон или статическое поле
  • Handler и Inner Class — вторая по частоте причина: неотменённые сообщения в очереди Looper держат ссылку на Activity
  • LeakCanary — стандартный инструмент автообнаружения; делает heap dump и показывает точную GC root chain
  • lifecycleScope и viewModelScope решают проблему утечек через корутины — автоматическая отмена при destroy
  • Профилируйте память в CI/CD: LeakCanary в debug + heap dump analysis в тестовом прогоне должны блокировать мерж при новых утечках

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

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

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

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