Minden, amit tudnod kell a memóriaszivárgásokról az Androidban.

Ali Asadi
Jun 30, 2019 – 16 min read

A Java egyik alapvető előnye, vagy pontosabban a JVM (Java Virtual Machine) egyik előnye a szemétgyűjtő (GC). Új objektumokat hozhatunk létre anélkül, hogy aggódnunk kellene a memóriából való felszabadításuk miatt. A szemétgyűjtő gondoskodik helyettünk a memória kiosztásáról és felszabadításáról.

Nem egészen! Megakadályozhatjuk, hogy a szemétgyűjtő felszabadítsa helyettünk a memóriát, ha nem értjük teljesen, hogyan működik a GC.

A kód írása anélkül, hogy jól értenénk, hogyan működik a GC, memóriaszivárgást okozhat az alkalmazásban. Ezek a szivárgások hatással lehetnek az alkalmazásunkra azáltal, hogy fel nem szabadított memóriát pazarolnak, és végül memórián kívüli kivételeket és késéseket okoznak.

Mi az a memóriaszivárgás?

A fel nem használt objektumok memóriából való felszabadításának sikertelensége

A fel nem használt objektumok memóriából való felszabadításának sikertelensége azt jelenti, hogy vannak olyan fel nem használt objektumok az alkalmazásban, amelyeket a GC nem tud törölni a memóriából.

Ha a GC nem tudja törölni a fel nem használt objektumokat a memóriából, akkor bajban vagyunk. A nem használt objektumokat tartalmazó memóriaegység az alkalmazás végéig vagy (a módszer végéig) foglalt lesz.

A módszer végéig? Igen, így van. Kétféle szivárgásunk van, olyan szivárgások, amelyek az alkalmazás végéig foglalják a memóriaegységet, és olyan szivárgások, amelyek a metódus végéig foglalják a memóriaegységet. Az első egyértelmű. A második még további tisztázásra szorul. Vegyünk egy példát a magyarázathoz! Tegyük fel, hogy van egy X metódusunk. Az X metódus valamilyen hosszú futású feladatot végez a háttérben, és egy percig tart, amíg befejezi. Továbbá, az X metódus közben nem használt objektumokat tart. Ebben az esetben a memóriaegység foglalt lesz, és a nem használt objektumokat egy percig nem lehet törölni a feladat végéig. A metódus befejezése után a GC törölheti a fel nem használt objektumokat, és visszaszerezheti a memóriát.

Ezt szeretném, ha egyelőre tudnád, erre később még visszatérünk némi kóddal és vizualizációval. JÓ MÓKA LESZ. 👹😜

Várj egy percet!!!🥴

Mielőtt a kontextusra ugranánk, kezdjük az alapokkal.

A RAM, vagy Random access memory, az androidos készülékekben vagy számítógépekben található memória, amely az aktuálisan futó alkalmazások és azok adatainak tárolására szolgál.

A RAM két főszereplőjét fogom ismertetni, az első a Heap, a második pedig a Stack. Térjünk át a szórakoztató részre 🤩🍻.

Heap & Stack

Nem fogom túl hosszúra nyújtani. Térjünk rögtön a lényegre, egy rövid leírás, a Stack a statikus memória kiosztására szolgál, míg a Heap a dinamikus memória kiosztására. Csak ne feledjük, hogy mind a Heap, mind a Stack a RAM-ban tárolódik.

Bővebben a Heap memóriáról

A Java Heap memóriát a virtuális gép használja az objektumok kiosztására. Amikor létrehozunk egy objektumot, az mindig a heapben jön létre. A virtuális gépek, mint a JVM vagy a DVM, rendszeres szemétgyűjtést (GC) végeznek, így a már nem hivatkozott objektumok heap-memóriája elérhetővé válik a jövőbeli allokációkhoz.

A zökkenőmentes felhasználói élmény biztosítása érdekében az Android minden futó alkalmazás számára kemény korlátot állít be a heap méretére. A heap méretének korlátja készülékenként változik, és azon alapul, hogy az adott eszköz mennyi RAM-mal rendelkezik. Ha az alkalmazásod eléri ezt a heap-határt, és megpróbál több memóriát kiosztani, akkor OutOfMemoryErrorjelzést kap, és leáll.

Gondolkoztál már azon, hogy mi az alkalmazásod heap-mérete?

Fedezzük fel ezt együtt. Az androidban van a Dalvik VM (DVM). A DVM egy egyedi, mobileszközökre optimalizált Java virtuális gép. Optimalizálja a virtuális gépet a memória, az akkumulátor élettartama és a teljesítmény szempontjából, és felelős az egyes alkalmazások memóriamennyiségének elosztásáért.

Beszéljünk a DVM két soráról:

  1. dalvik.vm.heapgrowthlimit: Ez a sor azon alapul, hogy a Dalvik hogyan indul el az alkalmazásod heapméretében. Ez az alapértelmezett heapméret minden alkalmazásnál. A maximum, amit az alkalmazásod elérhet!
  2. dalvik.vm.heapsize: Ez a sor a maximális heapméretet jelenti egy nagyobb heap esetén. Ezt úgy érheted el, hogy az alkalmazás manifesztjében nagyobb heapet kérsz az androidtól (android:largeHeap=”true”).

Ne használj nagyobb heapet az alkalmazásodban. Ezt CSAK akkor tedd meg, ha pontosan ismered ennek a lépésnek a mellékhatását. Itt adok neked elég információt, hogy tovább kutathass a témában.

Itt van egy táblázat, ami megmutatja, hogy milyen heap méretet kaptál az eszközöd RAM memóriája alapján:

+==========================+=========+=========+===================+
| DVM | 1GB RAM | 2GB RAM | 3GB RAM OR HIGHER |
+==========================+=========+=========+===================+
| DEFAULT(heapgrowthlimit) | 64m | 128m | 256m |
+--------------------------+---------+---------+-------------------+
| LARGE(heapsize) | 128m | 256m | 512m |
+--------------------------+---------+---------+-------------------+

Ne feledd, minél több ramod van, annál nagyobb lesz a heap mérete. észben tartva, hogy nem minden nagyobb rammal rendelkező eszköz 512m fölé megy, végezzen kutatást az eszközén, ha az eszközén több mint 3GB van, hogy lássa, ha a heap mérete nagyobb, mint 512m.

Hogyan tudja ellenőrizni az alkalmazás heap méretét az eszközén?

Az ActivityManager használatával. A maximális heap méretét futásidőben ellenőrizheti a getMemoryClass() vagy getLargeMemoryClass() metódusokkal (ha a nagy heap engedélyezve van).

  • getMemoryClass(): Visszaadja az alapértelmezett maximális heapméretet.
  • getLargeMemoryClass(): Visszaadja a maximálisan elérhető heap méretet, miután a manifesztben engedélyeztük a large heap flag-et.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Hogyan működik ez a való világban?

Ezzel az egyszerű alkalmazással fogjuk megérteni, hogy mikor használjuk a heap-et és mikor a stack-et.

alkalmazás

Az alábbi képen látható az alkalmazás heapjének és veremének ábrázolása, valamint az, hogy az egyes objektumok hova mutatnak és hol tárolódnak, amikor futtatjuk az alkalmazást.

Megnézzük az alkalmazás végrehajtását, megállítunk minden sort, elmagyarázzuk, hogy az alkalmazás mikor osztja ki az objektumokat, és mikor tárolja őket a heapben vagy a veremben. Azt is megnézzük, hogy az alkalmazás mikor szabadítja fel az objektumokat a veremről és a heapről.

  • 1. sor – A JVM létrehoz egy verem memóriablokkot a main metódus számára.

  • 2. sor – Ebben a sorban létrehozunk egy primitív helyi változót. A változót a main metódus stack memóriájában hozzuk létre és tároljuk.

  • 3. sor -Itt kérem a figyelmet!!! Ebben a sorban létrehozunk egy új objektumot. Az objektumot a main metódus stackjében hozzuk létre és a heapben tároljuk. A verem tárolja a hivatkozást, az objektum-heap memóriacímet (mutatót), míg a heap az eredeti objektumot.

  • 4. sor – Ugyanaz, mint a 3. sor.

  • 5. sor – A JVM létrehoz egy stack memóriablokkot a foo módszer számára.

  • 6. sor -Elkészít egy új objektumot. Az objektumot a foo metódus stack memóriájában hozzuk létre, és a stackben tároljuk a 3. sorban létrehozott objektum heap memóriacímét. Az értéket (a 3. sorban lévő objektum heap memóriacímét) az 5. sorban adtuk át. Ne feledjük, hogy a Java mindig értékkel adja át a hivatkozásokat.

  • 7. sor – Új objektumot hozunk létre. Az objektumot a veremben hoztuk létre, és a heapben lévő string poolra mutatunk.

  • 8. sor – A foo módszer utolsó sorában a módszer befejeződött. És az objektumok felszabadulnak a foo metódus stack blokkjából.

  • 9. sor- Ugyanaz, mint a 8. sorban, A main metódus utolsó sorában a metódus befejeződött. És a main metódus stack blokkja szabaddá válik.

Mi a helyzet a memória felszabadításával a heapről? Erre hamarosan rátérünk. Fogj egy kávét☕️, és folytasd 😼.

Mi történik, ha a módszerek megszűnnek?

Minden metódusnak megvan a saját hatósugara. Amikor a módszer befejeződik, az objektumok automatikusan felszabadulnak és visszakerülnek a veremről.

1. ábra

Az 1. ábrán, amikor a foo módszer befejeződött. A foo metódus stack memóriája vagy stack blokkja automatikusan felszabadul és visszakerül.

2. ábra

A 2. ábrán ugyanez. Amikor a main módszer befejeződött. A verem memóriája vagy a főmetódus veremblokkja automatikusan felszabadul és visszaszerződik.

Következtetés

Most már világos számunkra, hogy a veremben lévő objektumok ideiglenesek. Amint a metódus befejeződik, az objektumok felszabadulnak és visszakerülnek.

A verem egy LIFO (Last-In-First-Out) adatstruktúra. Úgy tekinthetünk rá, mint egy dobozra. Ennek a struktúrának a használatával a program könnyen kezelheti minden műveletét két egyszerű művelettel: push és pop.

Minden alkalommal, amikor el kell menteni valamit, például egy változót vagy módszert, a veremmutatót tolja és felfelé mozgatja. Minden alkalommal, amikor kilépsz egy metódusból, mindent leemel a veremmutatóról az előző metódus címére való visszatérésig. Példánkban a foo metódusból a main metódusba való visszatérés.

Mi van a halommal?

A halom különbözik a veremtől. Az objektumok felszabadításához és visszaköveteléséhez a heap memóriájából segítségre van szükségünk.

Ezért a Java, pontosabban a JVM egy szuperhőst készített a segítségünkre. Garbage Collectornak neveztük el. Ő fogja elvégezni helyettünk a nehéz munkát. És gondoskodik arról, hogy felismerje a nem használt objektumokat, felszabadítsa őket, és több helyet foglaljon vissza a memóriában.

Hogyan működik a szemétgyűjtő?

Egyszerű. A szemétgyűjtő a nem használt vagy elérhetetlen objektumokat keresi. Ha van olyan objektum a halomban, amelyre nem mutat semmilyen hivatkozás, a szemétgyűjtő gondoskodik arról, hogy kiengedje a memóriából, és több helyet igényeljen vissza.

GC

GC gyökerei a JVM által hivatkozott objektumok. Ezek a fa kezdeti objektumai. A fa minden objektumának van egy vagy több gyökérobjektuma. Amíg az alkalmazás vagy a GC gyökerek el tudja érni ezeket a gyökereket vagy ezeket az objektumokat, addig az egész fa elérhető. Amint elérhetetlenné válnak az alkalmazás vagy a GC gyökerek számára, nem használt objektumoknak vagy elérhetetlen objektumoknak minősülnek.

Mi történik, amikor a szemétgyűjtő fut?

Előre ez az alkalmazás aktuális memóriaállapota. A verem üres, és a halom tele van nem használt objektumokkal.

A GC előtt

A GC lefutása után az eredmények a következők lesznek:

A GC után

A GC felszabadítja és törli az összes nem használt objektumot a heapről.

Az ember! Mi lesz a memóriaszivárgással, amire várunk? LOL, csak még egy Lil kicsit, és máris ott leszünk. A mi egyszerű alkalmazásunkban az alkalmazás nagyszerűen és egyszerűen lett megírva. Nem volt semmi olyan hiba a kódban, ami megakadályozhatná a GC-t a heap objektumainak felszabadításában. És ezért a GC felszabadítja és visszaköveteli az összes objektumot a heap memóriájából. Folytassa tovább. A következő fejezetben rengeteg példát mutatunk a memóriaszivárgásokra😃.

Mikor és hogyan történnek a memóriaszivárgások?

Memóriaszivárgás akkor történik, amikor a verem még mindig hivatkozik a heapben lévő, nem használt objektumokra.

A fogalom jobb megértéséhez az alábbi képen egy egyszerű vizuális ábrázolás látható.

A vizuális ábrázoláson azt látjuk, hogy amikor a veremről hivatkozott, de már nem használt objektumok vannak. A szemétgyűjtő soha nem fogja felszabadítani vagy felszabadítani őket a memóriából, mert azt mutatja, hogy ezek az objektumok használatban vannak, miközben nincsenek.

Hogyan okozhatunk szivárgást?

Az Androidban többféleképpen is okozhatunk memóriaszivárgást. És ezt könnyen el lehet végezni AsyncTasks, Handlers, Singleton, Threads, és így tovább.

Mutatni fogok néhány példát a szálak, singleton és listeners használatával, hogy elmagyarázzam, hogyan okozhatunk szivárgást, és hogyan kerülhetjük el és javíthatjuk őket.

Nézze meg a Github tárolómat. Van néhány kódpélda.

Hogyan okozhatunk szivárgást a szálak használatával?

Ebben a példában elindítunk egy tevékenységet, amely egy szálat futtat a háttérben. A szál egy olyan feladatot fog elvégezni, amelynek a befejezése 20 másodpercig tart.

Mint ismeretes, A belső osztályok implicit hivatkozást tartanak a körülvevő osztályukra.

A színfalak mögött valójában így néz ki az aktivitás.

A DownloadTask a ThreadActivity-re való hivatkozást tartja.

Mi történik tehát a feladat vagy a szál elindítása után?

Az alkalmazás használatának két lehetséges folyamata van. Egy szabályos áramlás, amely hiba nélkül, az elvárásoknak megfelelően működik, és egy szivárgó áramlás, amely memóriaszivárgást okoz.

Reguláris áramlás

A képen az alkalmazás heapjét és stackjét mutatjuk be.

A felhasználó elindítja az alkalmazást, megnyitja a ThreadActivity-t, és a képernyőn várakozik a letöltési feladat befejezéséig. A felhasználó 20 másodpercet várt. Miért 20 másodperc? Mert ennyi idő alatt fejezi be a szál a feladatot.

A feladat fut

A feladat a háttérben fut. A felhasználó 20 másodpercet várt a letöltési feladat befejezésére. Amikor a feladat befejeződött, a verem felszabadítja a run() metódusblokkot.

Nincs hivatkozás a DownloadTask tartására. A GC a DownladTask objektumot nem használt objektumnak tekintette, és emiatt a következő GC-ciklusban törölni fogja a heap memóriájából.

A GC törli a nem használt objektumokat a heapből. Most, amikor a felhasználó bezárja a tevékenységet. A main metódus felszabadul a veremről, és a GC következő ciklusában a GC törli a ThreadActivity-t a heap memóriájából.

Tökéletes!

Tökéletes!