Изтичане на памет: какво е, типични сценарии и диагностика

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

Изтичане на памет (memory leak) — ситуация, при която приложението не освобождава паметта, заета от обекти, които вече не са необходими. В мобилното разработване това е особено критично: ограниченият heap и липсата на swap водят до OutOfMemoryError и срив на приложението. Според Purdue University (2022), 35% от Android приложенията в Google Play съдържат поне едно изтичане на памет. Ще разгледаме типични сценарии, диагностични инструменти и методи за отстраняване.

Основни точки

  • GC Root — входна точка, чрез която събирачът на отпадъци определя живите обекти
  • Изтичане на Context — предаване на Activity Context на singleton води до задържане на цялата View йерархия
  • 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 референции. Всеки обект, достижим чрез референции от тези корени, се счита за жив — дори ако разработчикът знае, че вече не е необходим.

kotlin
// Пример: статична колекция като GC Root — постоянно изтичане
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 не блокира GC — правилно поведение
    }
}

WeakReference решава проблема: GC игнорира слабите референции при определяне на живите обекти. Ако до обекта останат само слаби референции, той ще бъде събран в следващия GC цикъл.

Типични сценарии на изтичания в Android и iOS

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

Handler и изпратени съобщения — Handler.postDelayed(runnable, delay) поставя съобщение в опашката на Main Looper. Ако Activity е унищожено преди изтичането на закъснението, съобщението все още е в опашката и държи референция чрез 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) // задължително: изчистете опашката
        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 верига за всеки подозрителен обект.

Xcode Memory Graph Debugger — за iOS. Спира приложението и визуализира графа на обектите. Retain cycles се подчертават в червено, може да се кликне на всеки обект и да се види неговият retain count и референции.

ИнструментВъзможностиСложност
Memory ProfilerГрафика в реално време, heap dump, проследяване на алокации на обектиНиска
Eclipse MATDominator tree, Leak Suspects, OQL заявкиСредна
LeakCanaryАвтоматично откриване, следа на изтичане в известиеМинимална
Xcode Memory GraphВизуален граф на retain cycles, списък на живи обектиНиска

Според Uber Engineering Blog, внедряването на автоматично профилиране на паметта (LeakCanary + анализ на heap dump) в CI/CD тръбопровода намалява броя на инцидентите, свързани с памет, в продукция с 60% в рамките на 3 месеца.

Методи за отстраняване на изтичания

Замяна на Context — ако обектът живее по-дълго от Activity, използвайте applicationContext. Всички дългоживеещи обекти (singleton-и, хранилища, database helpers) трябва да получават Application Context, а не Activity Context. Изключение: UI компоненти, които се нуждаят от достъп до темата или ресурси, специфични за Activity.

Lifecycle-aware компоненти — използването на LifecycleObserver, DefaultLifecycleObserver или reactivex автоматично отменя абонаментите при onDestroy. Android Jetpack предоставя lifecycleScope и viewModelScope, които се почистват от съответното събитие.

Static inner class — ако вътрешният клас не се нуждае от достъп до полетата на външния, го направете static. Статичният вътрешен клас няма имплицитна референция към външния клас. Ако е необходим достъп, използвайте WeakReference за изрична референция.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Нестатичен вътрешен клас — имплицитна референция към MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Статичен вътрешен клас — без имплицитна референция
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

В iOS използвайте capture lists: [weak self] в затваряния, които могат да надживеят създателя. За делегати използвайте слаби референции (weak var delegate). За затваряния, които гарантирано се извикват само по време на живота на self, може да се използва [unowned self], но внимателно — достъпът до освободен обект ще доведе до срив.

Често задавани въпроси

Как да намеря изтичане без специални инструменти?

В 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. Ако не се извика recycle() на Bitmap или референцията не се нулира, native паметта няма да се освободи. Използвайте BitmapFactory с inSampleSize за зареждане на умалени копия и Glide/Coil за автоматично управление на кеша.

Какво е изтичане чрез статично поле?

Статичното поле — е GC Root. То живее, докато класът е зареден (в Android — докато Process е жив). Ако статично поле реферира Activity, Bitmap, View или друг тежък обект, този обект никога няма да бъде събран от GC. Статичното поле — вечна референция. Решение: съхранявайте само WeakReference или нулирайте статичното поле в 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 референции; всеки обект, достижим от тях, е жив
  • Изтичане на Context — най-масовият проблем в Android: предаване на Activity Context на singleton или статично поле
  • Handler и Inner Class — втората по честота причина: неотменени съобщения в опашката на Looper държат референция към Activity
  • LeakCanary — стандартният инструмент за автоматично откриване; прави heap dump и показва точната GC root верига
  • lifecycleScope и viewModelScope решават проблема с изтичания чрез корутини — автоматично отменяне при destroy
  • Профилирайте паметта в CI/CD: LeakCanary в debug + анализ на heap dump в тестово изпълнение трябва да блокират merge при нови изтичания

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също