Alles wat u moet weten over geheugenlekken in Android.

Ali Asadi
Jun 30, 2019 – 16 min read

Een van de kernvoordelen van Java, of om precies te zijn, van de JVM (Java Virtual Machine), is de garbage collector (GC). We kunnen nieuwe objecten maken zonder ons zorgen te hoeven maken over het vrijmaken van die objecten uit het geheugen. De garbage collector zorgt voor het toewijzen en vrijmaken van het geheugen voor ons.

Niet precies! We kunnen voorkomen dat de vuilnisman het geheugen voor ons vrijmaakt als we niet volledig begrijpen hoe de GC werkt.

Het schrijven van een code zonder een goed begrip van hoe de GC werkt kan geheugenlekken in de app maken. Deze lekken kunnen invloed hebben op onze app door verspilling van niet-vrijgegeven geheugen en uiteindelijk leidt tot uit het geheugen uitzonderingen en vertragingen.

Wat is Memory Leak?

Nalaten om ongebruikte objecten uit het geheugen vrij te geven

Nalaten om ongebruikte objecten uit het geheugen vrij te geven betekent dat er ongebruikte objecten in de applicatie zijn die de GC niet uit het geheugen kan wissen.

Wanneer de GC de ongebruikte objecten niet uit het geheugen kan wissen, zitten we in de problemen. De geheugeneenheid die de ongebruikte objecten bevat, zal bezet blijven tot het einde van de applicatie of (tot het einde van de methode).

Tot het einde van de methode? Yep, dat is juist. We hebben twee soorten lekken, lekken die de geheugeneenheid in beslag nemen tot het einde van de toepassing en lekken die de geheugeneenheid in beslag nemen tot het einde van de methode. De eerste is duidelijk. De tweede heeft meer verduidelijking nodig. Laten we een voorbeeld nemen om dit uit te leggen! Veronderstel dat we methode X hebben. Methode X is bezig met een lange taak op de achtergrond, en het zal een minuut duren voordat die klaar is. Ook houdt methode X ongebruikte objecten vast terwijl hij dit doet. In dat geval zal de geheugeneenheid bezet zijn, en de ongebruikte objecten kunnen een minuut lang niet gewist worden tot het einde van de taak. Na het beëindigen van de methode, kan de GC de ongebruikte objecten wissen en het geheugen terugwinnen.

Dat is wat ik je nu wil laten weten we komen hier later op terug met wat code en visualisatie. HET ZAL LEUK WORDEN. 👹😜

Wacht even!!🥴

Voordat we naar de context springen, laten we beginnen met de basis.

RAM, of Random Access Memory, is het geheugen in android apparaten of computers dat wordt gebruikt om de huidige draaiende applicaties en hun gegevens op te slaan.

Ik ga twee hoofdpersonen in het RAM uitleggen, de eerste is de Heap, en de tweede is de Stack. Laten we overgaan tot het leuke gedeelte 🤩🍻.

Heap & Stack

Ik ga het niet te lang maken. Laten we meteen ter zake komen, een korte beschrijving, de Stack wordt gebruikt voor statische geheugentoewijzing terwijl de Heap wordt gebruikt voor dynamische geheugentoewijzing. Houd in gedachten dat zowel de Heap als de Stack worden opgeslagen in het RAM.

Meer over het Heap geheugen

Java heap geheugen wordt gebruikt door de virtuele machine om objecten toe te wijzen. Wanneer je een object maakt, wordt het altijd in de heap gecreëerd. Virtuele machines, zoals JVM of DVM, voeren regelmatig garbage collection (GC) uit, waardoor het heapgeheugen van alle objecten waarnaar niet meer wordt verwezen, beschikbaar komt voor toekomstige toewijzingen.

Om een soepele gebruikerservaring te bieden, stelt Android een harde limiet aan de heapgrootte voor elke draaiende applicatie. De heap grootte limiet varieert tussen apparaten en is gebaseerd op hoeveel RAM een apparaat heeft. Als uw app deze heap limiet bereikt en probeert om meer geheugen toe te wijzen, zal het een OutOfMemoryError ontvangen en zal beëindigen.

Heb je je ooit afgevraagd wat de heap grootte voor uw toepassing is?

Laten we dit samen ontdekken. In Android, hebben we de Dalvik VM (DVM). De DVM is een unieke Java Virtual Machine geoptimaliseerd voor mobiele apparaten. Het optimaliseert de virtuele machine voor geheugen, levensduur van de batterij, en de prestaties, en het is verantwoordelijk voor het verdelen van de hoeveelheid geheugen voor elke toepassing.

Laten we praten over twee lijnen in de DVM:

  1. dalvik.vm.heapgrowthlimit: Deze lijn is gebaseerd op hoe Dalvik zal beginnen in de heap grootte van uw toepassing. Het is de standaard heap grootte voor elke applicatie. Het maximum dat uw applicatie kan bereiken!
  2. dalvik.vm.heapsize: Deze regel vertegenwoordigt de maximale heap grootte voor een grotere heap. Je kunt dat bereiken door android om een grotere heap te vragen in je applicatie manifest (android:largeHeap=”true”).

Gebruik geen grotere heap in je app. Doe dat ALLEEN als je precies weet wat het neveneffect van deze stap is. Hier geef ik je genoeg informatie om verder onderzoek te doen naar het onderwerp.

Hier is een tabel die laat zien welke heap grootte je hebt op basis van het RAM van je apparaat:

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

Houd in gedachten dat hoe meer ram je hebt, hoe hoger de heap grootte zal zijn. Houd in gedachten dat niet alle apparaten met een hogere ram gaan boven 512m doe je onderzoek op uw apparaat als uw apparaat heeft meer dan 3 GB om te zien of uw heap grootte groter is dan 512m.

Hoe kun je controleren of de app heap grootte voor uw apparaat?

Met behulp van de ActivityManager. U kunt de maximale heapgrootte tijdens runtime controleren met behulp van de methoden getMemoryClass() of getLargeMemoryClass() (wanneer een grote heap is ingeschakeld).

  • getMemoryClass(): Geeft de standaard maximale heap grootte.
  • getLargeMemoryClass(): Geeft de maximaal beschikbare heap grootte na het inschakelen van de large heap flag in het manifest.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Hoe werkt het in de echte wereld?

We zullen deze eenvoudige applicatie gebruiken om te begrijpen wanneer we de heap gebruiken en wanneer we de stack gebruiken.

applicatie

De onderstaande afbeelding toont een weergave van de heap en stack van de app en waar elk object naartoe wijst en wordt opgeslagen wanneer we de app uitvoeren.

We zullen de uitvoering van de app doornemen, elke regel stoppen, uitleggen wanneer de toepassing de objecten toewijst en ze opslaat in de heap of de stack. We zullen ook zien wanneer de app de objecten vrijgeeft van de stack en de heap.

  • Regel 1 – De JVM maakt een stackgeheugenblok voor de hoofdmethode.

  • Regel 2 – In deze regel maken we een primitieve lokale variabele aan. De variabele wordt aangemaakt en opgeslagen in het stackgeheugen van de hoofdmethode.

  • Regel 3 -Hier moet je aandacht even naar uitgaan!!! In deze regel maken we een nieuw object. Het object wordt gemaakt in de stack van de hoofdmethode en opgeslagen in de heap. De stack slaat de referentie op, het object-heap geheugenadres (pointer), terwijl de heap het originele object opslaat.

  • Lijn 4 -Hetzelfde als lijn 3.

  • Regel 5 – De JVM maakt een stackgeheugenblok voor de methode foo.

  • Regel 6 -Er wordt een nieuw object aangemaakt. Het object wordt aangemaakt in het stackgeheugen van de methode foo, en we slaan in de stack het heapgeheugenadres op van het object dat we in regel 3 hebben aangemaakt. De waarde (heap-geheugenadres van het object in regel 3) hebben we doorgegeven in regel 5. Houd in gedachten dat Java altijd referenties doorgeeft door middel van waarde.

  • Regel 7 – We maken een nieuw object. Het object wordt aangemaakt in de stack en wijst naar de string pool in de heap.

  • Regel 8 – In de laatste regel van de foo methode wordt de methode beëindigd. En de objecten worden vrijgegeven uit het stackblok van de methode foo.

  • Regel 9- Hetzelfde als regel 8, In de laatste regel van de methode main, is de methode beëindigd. En het stackblok van de hoofdmethode wordt vrij.

Hoe zit het met het vrijmaken van het geheugen van de heap? Daar komen we zo op. Pak een koffie☕️, en ga door 😼.

Wat gebeurt er als methodes worden beëindigd?

Elke methode heeft zijn eigen scope. Wanneer de methode wordt beëindigd, worden de objecten vrijgegeven en automatisch van de stack opgeëist.

figuur 1

In figuur 1, wanneer de methode foo wordt beëindigd. Het stackgeheugen of het stackblok van de methode foo wordt automatisch vrijgegeven en teruggehaald.

figuur 2

In figuur 2, idem. Wanneer de methode main wordt beëindigd. Het stackgeheugen of het stackblok van de hoofdmethode wordt automatisch vrijgegeven en hergebruikt.

Conclusie

Nu is het ons duidelijk dat de objecten in de stack tijdelijk zijn. Zodra de methode wordt beëindigd, worden de objecten vrijgegeven en opgeëist.

De stack is een LIFO-gegevensstructuur (Last-In-First-Out). Je kunt het zien als een doos. Door deze structuur te gebruiken, kan het programma al zijn bewerkingen gemakkelijk beheren met behulp van twee eenvoudige bewerkingen: push en pop.

Telkens wanneer u iets moet opslaan, zoals een variabele of methode, wordt de stack-pointer gepusht en omhoog verplaatst. Telkens wanneer je een methode verlaat, wordt alles van de stack pointer afgehaald tot de terugkeer naar het adres van de vorige methode. In ons voorbeeld, terugkeren van de foo methode naar de main methode.

Hoe zit het met de Heap?

De heap is anders dan de stack. Om objecten uit het heap-geheugen vrij te maken en weer op te eisen, hebben we hulp nodig.

Daarvoor heeft Java, of om precies te zijn, de JVM een superheld gemaakt om ons te helpen. We noemen het de Garbage Collector. Hij gaat het zware werk voor ons doen. En zorgt ervoor dat ongebruikte objecten worden opgespoord, worden vrijgegeven en meer ruimte in het geheugen wordt teruggewonnen.

Hoe werkt de garbage collector?

Simpel. De vuilnisman gaat op zoek naar ongebruikte of onbereikbare objecten. Als er een object in de heap is waarnaar niet wordt verwezen, zorgt de vuilnisman ervoor dat het uit het geheugen wordt verwijderd en dat er meer ruimte vrijkomt.

GC

GC-rootobjecten zijn objecten waarnaar wordt verwezen door de JVM. Het zijn de initiële objecten van de boomstructuur. Elk object in de boom heeft een of meer root-objecten. Zolang de toepassing of de GC-wortels deze wortels of deze objecten kunnen bereiken, is de hele boom bereikbaar. Zodra ze onbereikbaar worden voor de applicatie of de GC root, worden ze beschouwd als ongebruikte objecten of onbereikbare objecten.

Wat gebeurt er als de garbage collector draait?

Voorlopig is dit de huidige geheugenstatus van de applicatie. De stack is leeg, en de heap is vol met ongebruikte objecten.

Vóór GC

Na het uitvoeren van de GC zullen de resultaten als volgt zijn:

Na GC

De GC zal alle ongebruikte objecten van de heap vrijgeven en wissen.

Man! Hoe zit het met het geheugenlek waar we op zitten te wachten? LOL, nog een klein beetje, en we zullen er zijn. In onze eenvoudige applicatie, was de app geweldig en eenvoudig geschreven. Er was niets mis met de code die de GC kan verhinderen om de objecten van de heap vrij te geven. En om die reden, geeft de GC alle objecten vrij en eist ze op van het heap geheugen. Ga zo door. We hebben een heleboel voorbeelden van geheugenlekken in de volgende sectie😃.

Wanneer en hoe gebeuren geheugenlekken?

Een geheugenlek gebeurt wanneer de stack nog verwijzingen heeft naar ongebruikte objecten in de heap.

Er is een eenvoudige visuele weergave in de afbeelding hieronder voor een beter begrip van het concept.

In de visuele weergave zien we dat wanneer we objecten hebben waarnaar wordt verwezen in de stack, maar die niet meer in gebruik zijn. De vuilnisman zal nooit vrij te geven of ze uit het geheugen, want het toont aan dat die objecten in gebruik zijn, terwijl ze niet.

Hoe kunnen we een lek veroorzaken?

Er zijn verschillende manieren om een geheugenlek in Android veroorzaken. En het kan gemakkelijk worden gemaakt met behulp van AsyncTasks, Handlers, Singleton, Threads, en nog veel meer.

Ik zal een aantal voorbeelden laten zien met behulp van threads, singleton, en luisteraars om uit te leggen hoe we een lek kunnen veroorzaken en te voorkomen en te repareren.

Check out mijn Github repository. Ik heb een aantal code voorbeelden.

Hoe kunnen we een lek veroorzaken met behulp van thread?

In dit voorbeeld, gaan we een activiteit starten die een thread op de achtergrond laat lopen. De thread gaat een taak uitvoeren die 20 seconden duurt.

Zoals bekend, houden de binnenklassen een impliciete verwijzing naar hun omhullende klasse.

Achter de schermen ziet de activiteit er in feite zo uit.

De DownloadTask heeft een verwijzing naar de ThreadActivity.

Wat gebeurt er na het starten van de taak of de thread?

Er zijn twee mogelijke stromen voor het gebruik van de toepassing. Een normale stroom die werkt zoals verwacht zonder fouten en een lek stroom die een geheugenlek veroorzaken.

Reguliere stroom

In de afbeelding, presenteren we de heap en de stack van de toepassing.

De gebruiker start de applicatie, opent de ThreadActivity, en wachtte op het scherm tot de downloadtaak was voltooid. De gebruiker heeft 20 seconden gewacht. Waarom 20-seconden? Omdat dit de tijd is die de thread nodig heeft om de taak te voltooien.

Taak wordt uitgevoerd

De taak wordt op de achtergrond uitgevoerd. De gebruiker heeft 20 seconden gewacht op de voltooiing van de downloadtaak. Wanneer de taak is voltooid, geeft de stack het blok van de run()-methode vrij.

Er is geen referentie die de DownloadTask vasthoudt. De GC beschouwde het object DownladTask als een ongebruikt object, en om die reden zal het in de volgende GC-cyclus uit het heap-geheugen worden verwijderd.

De GC verwijdert de ongebruikte objecten uit de heap. Nu, wanneer de gebruiker de activiteit sluit. De hoofdmethode wordt vrijgegeven van de stack, en in de volgende cyclus van de GC, zal de GC de ThreadActivity van het heap geheugen verwijderen.

Perfect!