Scurgerea de memorie: ce este, scenarii tipice și diagnosticare

Autor: IT Sectr Publicat: 2026-07-29 Timp de citire: 10 min

Scurgerea de memorie (memory leak) — situația în care aplicația nu eliberează memoria ocupată de obiecte care nu mai sunt necesare. În dezvoltarea mobilă, acest lucru este deosebit de critic: heap-ul limitat și lipsa swap-ului duc la OutOfMemoryError și la prăbușirea aplicației. Conform Purdue University (2022), 35% dintre aplicațiile Android din Google Play conțin cel puțin o scurgere de memorie. Vom analiza scenariile tipice, instrumentele de diagnosticare și metodele de eliminare.

Principalele puncte

  • GC Root — punctul de intrare prin care colectorul de gunoi determină obiectele vii
  • Scurgerea Context — transmiterea Activity Context către un singleton duce la reținerea întregii ierarhii View
  • Handler cu postDelayed — dacă Activity este distrus, Handler nu îi permite să ajungă la GC
  • Heap dump — metoda principală de analiză a scurgerilor prin MAT sau Android Profiler
  • SoftReference — alternativă la WeakReference pentru cache-uri cu curățare automată la lipsa de memorie

Ce este scurgerea de memorie în aplicațiile mobile?

Scurgerea de memorie — este situația în care memoria alocată nu este returnată sistemului după ce obiectul nu mai este necesar programului. Colectorul de gunoi consideră un astfel de obiect viu, deoarece către el duce un lanț activ de referințe de la GC Root.

În Java/Kotlin, colectorul de gunoi funcționează automat, dar nu poate determina că obiectul nu este necesar logic dacă există o referință tehnică la el. Dezvoltatorul trebuie să întrerupă explicit conexiunile inutile. În Swift/Objective-C, ARC numără automat referințele, dar retain cycles blochează resetarea contorului.

Principalul pericol al scurgerilor — efectul cumulativ. Fiecare scurgere consumă o cantitate mică de memorie, dar la tranziții multiple între ecrane (rotiri ale ecranului, deschiderea/închiderea Activity), scurgerile se acumulează până când limita heap-ului este epuizată.

Cu ce diferă scurgerea de umflare?

Scurgerea — obiectul este inaccesibil codului, dar nu a fost șters de GC. Umflarea — obiectul este necesar logic, dar este stocat în cantitate excesivă. Exemplu de umflare: un cache de imagini de 100 MB cu un set de lucru de 30 MB. Ambele probleme duc la OOM, dar cauzele și metodele de tratament sunt diferite.

Cum funcționează colectorul de gunoi și de ce apar scurgerile?

ART (Android Runtime) utilizează colectarea generațională a gunoiului cu concurrent compaction. Memoria este împărțită în generația tânără (Young), cea veche (Old) și obiecte mari (Large). Obiectele care au supraviețuit mai multor cicluri GC sunt mutate în Old generation, unde colectarea are loc mai rar — aceasta accelerează ciclurile obișnuite.

GC pornește când heap-ul atinge un anumit prag de ocupare (de obicei 75-85%). În timpul GC, toate firele de execuție ale aplicației sunt suspendate (STW — Stop The World). Cu cât sunt mai multe obiecte vii, cu atât pauza este mai lungă. Scurgerile măresc numărul de obiecte vii, prelungind pauzele GC.

Colectorul determină obiectele vii parcurgând graful de la GC Roots: câmpuri statice, variabile de stivă ale firelor active, referințe JNI. Orice obiect accesibil prin referințe de la aceste rădăcini este considerat viu — chiar dacă dezvoltatorul știe că nu mai este necesar.

kotlin
// Exemplu: colecție statică ca GC Root — scurgere permanentă
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 nu blochează GC — comportament corect
    }
}

WeakReference rezolvă problema: GC ignoră referințele slabe la determinarea obiectelor vii. Dacă asupra unui obiect au rămas doar referințe slabe, acesta va fi colectat în următorul ciclu GC.

Scenarii tipice de scurgeri în Android și iOS

Activity Context — cel mai răspândit scenariu de scurgeri în Android. Dacă un singleton, un câmp static sau un serviciu de lungă durată stochează o referință la Activity Context, întreaga Activity cu toate View-urile nu poate fi colectată de GC. Soluție: utilizați Application Context pentru obiectele de lungă durată.

Handler și mesajele trimise — Handler.postDelayed(runnable, delay) plasează un mesaj în coada Main Looper. Dacă Activity este distrusă înainte de expirarea întârzierii, mesajul este încă în coadă și menține referința prin Runnable → clasă anonimă → clasă exterioară (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) // obligatoriu: curățați coada
        super.onPause()
    }
}

Inner Classes — o clasă interioară non-statică are o referință implicită la instanța clasei exterioare. Dacă clasa exterioară este Activity, iar clasa interioară a fost transmisă undeva în afară (de exemplu, în RecyclerView.Adapter), Activity nu poate fi colectată.

  • TimerTask și ScheduledExecutorService — sarcini planificate înainte de distrugerea Activity
  • BroadcastReceiver — neînregistrat în onPause/onDestroy continuă să mențină Context-ul
  • ViewModel cu referință la View — ViewModel supraviețuiește Activity, referința la View duce la scurgere
  • Retrofit Call — dacă Call nu este anulat, răspunsul ajunge la Fragment-ul distrus

Instrumente de diagnosticare a scurgerilor de memorie

Android Studio Memory Profiler — instrumentul încorporat pentru monitorizarea heap-ului în timp real. Afișează graficul memoriei ocupate, numărul de alocări și obiecte pe tipuri. Permite înregistrarea unui heap dump și exportul în format HPROF pentru analiză în MAT.

Eclipse MAT (Memory Analyzer Tool) — analizor desktop de heap dump. Construiește automat Leak Suspects Report, care evidențiază obiectele cu cel mai mare retained size și sugerează un posibil lanț GC root pentru fiecare obiect suspect.

Xcode Memory Graph Debugger — pentru iOS. Suspendă aplicația și vizualizează graful obiectelor. Retain cycles sunt evidențiate cu roșu, se poate face clic pe orice obiect pentru a vedea retain count și referințele sale.

InstrumentCapabilitățiComplexitate
Memory ProfilerGrafic în timp real, heap dump, Urmărirea alocării obiectelorScăzută
Eclipse MATArborele dominator, Suspecți de scurgere, interogări OQLMedie
LeakCanaryDetectare automată, urma scurgerii în notificareMinimă
Xcode Memory GraphGraf vizual al retain cycles, listă de obiecte viiScăzută

Conform Uber Engineering Blog, implementarea profilării automate a memoriei (LeakCanary + analiza heap dump) în pipeline-ul CI/CD reduce numărul de incidente legate de memorie în producție cu 60% în decurs de 3 luni.

Metode de eliminare a scurgerilor

Înlocuirea Context — dacă obiectul trăiește mai mult decât Activity, utilizați applicationContext. Toate obiectele de lungă durată (singletoni, repository-uri, database helpers) trebuie să primească Application Context, nu Activity Context. Excepție: componentele UI care au nevoie de acces la tema sau resursele specifice Activity.

Componente Lifecycle-aware — utilizarea LifecycleObserver, DefaultLifecycleObserver sau reactivex anulează automat abonamentele la onDestroy. Android Jetpack oferă lifecycleScope și viewModelScope, care sunt curățate de evenimentul corespunzător.

Static inner class — dacă clasa interioară nu are nevoie de acces la câmpurile clasei exterioare, faceți-o static. O clasă interioară statică nu are o referință implicită la clasa exterioară. Dacă accesul este necesar, utilizați WeakReference pentru o referință explicită.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Clasă interioară non-statică — referință implicită la MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Clasă interioară statică — fără referință implicită
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

În iOS, utilizați capture lists: [weak self] în închideri care pot supraviețui creatorului. Pentru delegați, utilizați referințe slabe (weak var delegate). Pentru închiderile care sunt garantat apelate doar în timpul vieții lui self, se poate utiliza [unowned self], dar cu prudență — accesarea unui obiect eliberat va cauza un crash.

Întrebări frecvente

Cum găsesc o scurgere fără instrumente speciale?

În Android, efectuați câteva tranziții între ecrane (Activity A → B → A → B) și verificați adb shell dumpsys meminfo package_name. Dacă Total PSS crește constant și nu revine la valoarea inițială — există o scurgere. În iOS, similar: utilizați Debug Memory Graph în Xcode pentru verificare vizuală.

Poate o corutină Kotlin să provoace o scurgere?

Da, dacă CoroutineScope nu este anulat la distrugerea componentului. O corutină lansată în GlobalScope continuă să se execute chiar și după finish() al Activity. Soluție: utilizați viewModelScope (anulat în onCleared) sau lifecycleScope (anulat în onDestroy). Pentru Scope-uri proprii, creați lifecycle-aware scopes prin LifecycleOwner.

Cum afectează Bitmap scurgerile?

Bitmap stochează datele pixelilor în heap-ul nativ, nu în Java heap. Aceasta înseamnă că Java GC nu vede dimensiunea reală a Bitmap-ului. Dacă Bitmap nu este apelat cu recycle() sau referința nu este anulată, memoria nativă nu va fi eliberată. Utilizați BitmapFactory cu inSampleSize pentru încărcarea copiilor reduse și Glide/Coil pentru gestionarea automată a cache-ului.

Ce este o scurgere printr-un câmp static?

Câmpul static — este un GC Root. Trăiește cât timp clasa este încărcată (în Android — cât timp trăiește Process). Dacă un câmp static face referință la Activity, Bitmap, View sau orice alt obiect greu, acest obiect nu va fi niciodată colectat de GC. Câmpul static — o referință eternă. Soluție: stocați doar WeakReference sau anulați câmpul static în onDestroy.

Cum să evitați scurgerile în iOS cu ARC?

ARC eliberează automat obiectele când contorul de referințe tari scade la zero. Retain cycle — singurul mod de scurgere în ARC. Utilizați întotdeauna weak pentru referințele parent→child, unde child trebuie să supraviețuiască parent-ului (delegați, data source). Pentru închideri, utilizați capture list [weak self] și verificați self pentru nil în interiorul închiderii.

Concluzii

  • Scurgerea de memorie — obiectul este inaccesibil codului, dar nu este șters de GC deoarece există o referință activă de la GC Root
  • GC Roots includ câmpuri statice, variabile de stivă și referințe JNI; orice obiect accesibil de la ele este viu
  • Scurgerea Context — cea mai răspândită problemă în Android: transmiterea Activity Context către un singleton sau câmp static
  • Handler și Inner Class — a doua cauză ca frecvență: mesajele neanulate în coada Looper mențin referința la Activity
  • LeakCanary — instrumentul standard de detectare automată; face heap dump și arată lanțul exact GC root
  • lifecycleScope și viewModelScope rezolvă problema scurgerilor prin corutine — anulare automată la destroy
  • Profilați memoria în CI/CD: LeakCanary în debug + analiza heap dump în rularea testelor ar trebui să blocheze merge-ul la scurgeri noi

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și