Memóriaszivárgás (memory leak) — olyan helyzet, amikor az alkalmazás nem szabadítja fel a már nem szükséges objektumok által elfoglalt memóriát. Mobilfejlesztésben ez különösen kritikus: a korlátozott heap és a swap hiánya OutOfMemoryError-hoz és az alkalmazás összeomlásához vezet. A Purdue University (2022) adatai szerint a Google Play-ben lévő Android-alkalmazások 35%-a tartalmaz legalább egy memóriaszivárgást. Tekintsük át a tipikus forgatókönyveket, diagnosztikai eszközöket és megszüntetési módszereket.
Főbb pontok
Memóriaszivárgás — olyan helyzet, amikor a lefoglalt memória nem kerül vissza a rendszerbe, miután az objektumra már nincs szüksége a programnak. A szemétgyűjtő az ilyen objektumot élőnek tekinti, mert aktív referencialánc vezet hozzá a GC Root-tól.
Java/Kotlin nyelven a szemétgyűjtő automatikusan működik, de nem tudja megállapítani, hogy egy objektum logikailag nem szükséges, ha technikai referencia létezik rá. A fejlesztőnek explicit módon meg kell szakítania a felesleges kapcsolatokat. Swift/Objective-C nyelven az ARC automatikusan számolja a referenciákat, de a retain cycles blokkolják a számláló nullázását.
A szivárgások fő veszélye a kumulatív hatás. Minden szivárgás kis mennyiségű memóriát fogyaszt, de többszörös képernyőváltásoknál (képernyőforgatás, Activity megnyitása/bezárása) a szivárgások felhalmozódnak, amíg a heap korlátja ki nem merül.
Szivárgás — az objektum nem elérhető a kód számára, de a GC nem távolította el. Felfúvódás — az objektum logikailag szükséges, de túlzott mennyiségben van tárolva. Példa felfúvódásra: 100 MB-os képgyorsítótár 30 MB-os munkakészlettel. Mindkét probléma OOM-hoz vezet, de az okok és a kezelési módszerek különbözőek.
ART (Android Runtime) generációs szemétgyűjtést használ concurrent compaction-sel. A memória fiatal generációra (Young), öregre (Old) és hatalmas objektumokra (Large) oszlik. Azok az objektumok, amelyek több GC-ciklust is túléltek, az Old generation-be kerülnek, ahol ritkábban történik gyűjtés — ez felgyorsítja a szokásos ciklusokat.
A GC akkor indul, amikor a heap elér egy bizonyos telítettségi küszöböt (általában 75-85%). A GC alatt az alkalmazás összes szálának futása felfüggesztésre kerül (STW — Stop The World). Minél több az élő objektum, annál hosszabb a szünet. A szivárgások növelik az élő objektumok számát, meghosszabbítva a GC-szüneteket.
A gyűjtő az élő objektumokat a GC Roots-tól indulva bejárva határozza meg: statikus mezők, aktív szálak vermi változói, JNI-referenciák. Bármely objektum, amely e gyökerektől referenciákon keresztül elérhető, élőnek minősül — még akkor is, ha a fejlesztő tudja, hogy már nincs rá szükség.
// Példa: statikus gyűjtemény mint GC Root — állandó szivárgás
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 nem blokkolja a GC-t — helyes viselkedés
}
}
WeakReference megoldja a problémát: a GC figyelmen kívül hagyja a gyenge referenciákat az élő objektumok meghatározásakor. Ha egy objektumra csak gyenge referenciák maradnak, a következő GC-ciklusban összegyűjtésre kerül.
Activity Context — a legelterjedtebb szivárgási forgatókönyv Androidon. Ha egy singleton, statikus mező vagy hosszú életű szolgáltatás tárol referenciát az Activity Context-re, akkor a teljes Activity az összes View-val együtt nem gyűjthető be a GC által. Megoldás: hosszú életű objektumokhoz használj Application Context-et.
Handler és elküldött üzenetek — a Handler.postDelayed(runnable, delay) üzenetet helyez a Main Looper sorába. Ha az Activity a késleltetés lejárta előtt megsemmisül, az üzenet még mindig a sorban van, és megtartja a referenciát a Runnable → névtelen osztály → külső osztály (Activity) útvonalon keresztül.
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) // kötelező: sor törlése
super.onPause()
}
}
Inner Classes — a nem statikus belső osztály implicit referenciával rendelkezik a külső osztály példányára. Ha a külső osztály Activity, és a belső osztályt átadják valahova (pl. RecyclerView.Adapter-be), az Activity nem gyűjthető be.
Android Studio Memory Profiler — a beépített eszköz a heap valós idejű monitorozására. Megjeleníti a foglalt memória grafikonját, a foglalások és objektumok számát típusonként. Lehetővé teszi heap dump rögzítését és HPROF formátumba exportálását MAT-ban való elemzéshez.
Eclipse MAT (Memory Analyzer Tool) — asztali heap dump elemző. Automatikusan elkészíti a Leak Suspects Report-ot, amely kiemeli a legnagyobb retained size-dzsel rendelkező objektumokat, és minden gyanús objektumhoz javasol egy lehetséges GC root láncot.
Xcode Memory Graph Debugger — iOS-hez. Felfüggeszti az alkalmazást és vizualizálja az objektumgráfot. A retain cycles pirossal vannak kiemelve, bármely objektumra kattintva megtekinthető a retain count és a referenciák.
| Eszköz | Funkciók | Komplexitás |
|---|---|---|
| Memory Profiler | Valós idejű grafikon, heap dump, objektumfoglalás nyomon követése | Alacsony |
| Eclipse MAT | Dominator fa, szivárgásgyanús esetek, OQL lekérdezések | Közepes |
| LeakCanary | Automatikus észlelés, szivárgás nyomvonala az értesítésben | Minimális |
| Xcode Memory Graph | Retain cycles vizuális gráfja, élő objektumok listája | Alacsony |
Az Uber Engineering Blog szerint az automatikus memóriaprofilálás (LeakCanary + heap dump elemzés) CI/CD pipeline-ban történő bevezetése 60%-kal csökkenti a memóriával kapcsolatos incidensek számát éles környezetben 3 hónapon belül.
Context cseréje — ha az objektum tovább él, mint az Activity, használj applicationContext-et. Minden hosszú életű objektumnak (singletonok, repository-k, database helperek) Application Context-et kell kapnia, nem Activity Context-et. Kivétel: UI-komponensek, amelyeknek az Activity-specifikus témához vagy erőforrásokhoz kell hozzáférniük.
Lifecycle-aware komponensek — a LifecycleObserver, DefaultLifecycleObserver vagy reactivex használata automatikusan törli a feliratkozásokat az onDestroy-nél. Az Android Jetpack lifecycleScope-ot és viewModelScope-ot biztosít, amelyeket a megfelelő esemény tisztít.
Static inner class — ha a belső osztálynak nincs szüksége hozzáférésre a külső osztály mezőihez, tedd static-ké. A statikus belső osztálynak nincs implicit referenciája a külső osztályra. Ha hozzáférés szükséges, használj WeakReference-t az explicit referenciához.
class MyActivity : AppCompatActivity() {
// ❌ Nem statikus belső osztály — implicit referencia a MyActivity-re
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Statikus belső osztály — nincs implicit referencia
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
iOS-ben használj capture list-eket: [weak self] azokban a closure-ökben, amelyek túlélhetik a létrehozójukat. Delegáltakhoz használj gyenge referenciákat (weak var delegate). Azon closure-ökhöz, amelyek garantáltan csak a self élettartama alatt hívódnak meg, használható [unowned self], de óvatosan — a felszabadított objektum elérése crash-t okoz.
Gyakran ismételt kérdések
Android-ban végezz el néhány váltást a képernyők között (Activity A → B → A → B), és ellenőrizd az adb shell dumpsys meminfo package_name parancsot. Ha a Total PSS folyamatosan nő, és nem tér vissza az eredeti értékre — szivárgás van. iOS-ben hasonlóan: használd a Debug Memory Graph-ot az Xcode-ban a vizuális ellenőrzéshez.
Igen, ha a CoroutineScope nincs törölve a komponens megsemmisülésekor. A GlobalScope-ban elindított korutin a Activity finish() után is folytatja a végrehajtást. Megoldás: használj viewModelScope-ot (onCleared-ben törlődik) vagy lifecycleScope-ot (onDestroy-ben törlődik). Saját Scope-okhoz hozz létre lifecycle-aware scope-okat LifecycleOwner-en keresztül.
A Bitmap a pixeladatokat natív heap-ben tárolja, nem a Java heap-ben. Ez azt jelenti, hogy a Java GC nem látja a Bitmap valós méretét. Ha a Bitmap-en nem hívják meg a recycle()-t vagy a referencia nem nullázódik, a natív memória nem szabadul fel. Használj BitmapFactory-t inSampleSize-szel a kicsinyített másolatok betöltéséhez, és Glide/Coil-t az automatikus gyorsítótár-kezeléshez.
A statikus mező — egy GC Root. Addig él, amíg az osztály be van töltve (Android-ben — amíg a Process él). Ha egy statikus mező Activity-re, Bitmap-re, View-ra vagy bármely más nehéz objektumra hivatkozik, ez az objektum soha nem lesz begyűjtve a GC által. A statikus mező — örök referencia. Megoldás: csak WeakReference-t tárolj, vagy nullázd a statikus mezőt az onDestroy-ban.
Az ARC automatikusan felszabadítja az objektumokat, amikor az erős referenciák számlálója nullára csökken. A retain cycle — az egyetlen szivárgási mód ARC-ben. Mindig használj weak-et a parent→child referenciákhoz, ahol a child-nak túl kell élnie a parent-et (delegáltak, data source). A closure-ökhöz használj capture list-et: [weak self], és ellenőrizd a self-et nil-ra a closure-on belül.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is