Alt, hvad du har brug for at vide om hukommelseslækager i Android.

Ali Asadi
Jun 30, 2019 – 16 min read

En af de centrale fordele ved Java, eller for at være mere præcis, af JVM’en (Java Virtual Machine), er garbage collector (GC). Vi kan oprette nye objekter uden at skulle bekymre os om at frigøre dem fra hukommelsen. Garbage collector sørger for at allokere og frigøre hukommelsen for os.

Det er ikke helt rigtigt! Vi kan forhindre garbage collector i at frigøre hukommelsen for os, hvis vi ikke helt forstår, hvordan GC’en fungerer.

Skrivning af en kode uden en god forståelse af, hvordan GC’en fungerer, kan medføre hukommelseslækager i appen. Disse lækager kan påvirke vores app ved at spilde ufrigjort hukommelse og i sidste ende forårsage out of memory undtagelser og forsinkelser.

Hvad er hukommelseslækage?

Svigt i at frigive ubrugte objekter fra hukommelsen

Svigt i at frigive ubrugte objekter fra hukommelsen betyder, at der er ubrugte objekter i applikationen, som GC ikke kan rydde fra hukommelsen.

Når GC ikke kan rydde de ubrugte objekter fra hukommelsen, er vi i problemer. Den hukommelsesenhed, der indeholder de ubrugte objekter, vil være optaget indtil afslutningen af programmet eller (indtil afslutningen af metoden).

Indtil afslutningen af metoden? Jep, det er rigtigt. Vi har to slags lækager, lækager, der optager hukommelsesenheden indtil afslutningen af programmet, og lækager, der optager hukommelsesenheden indtil afslutningen af metoden. Den første er klar. Den anden kræver mere præcisering. Lad os tage et eksempel for at forklare det! Lad os antage, at vi har metode X. Metode X udfører en eller anden lang løbeopgave i baggrunden, og det vil tage et minut at afslutte den. Metode X holder også ubrugte objekter, mens den gør dette. I så fald vil hukommelsesenheden være optaget, og de ubrugte objekter kan ikke ryddes i et minut, før opgaven er afsluttet. Efter metodens afslutning kan GC rydde de ubrugte objekter og genvinde hukommelsen.

Det er hvad jeg vil have dig til at vide for nu vi vil vende tilbage til dette senere med noget kode og visualisering. DET BLIVER SJOVT. 👹😜

Wait a minute!!!🥴

Hvor vi springer over til konteksten, skal vi starte med det grundlæggende.

RAM, eller Random Access Memory, er den hukommelse i android-enheder eller computere, der bruges til at gemme aktuelle kørende programmer og deres data.

Jeg vil forklare to hovedpersoner i RAM’en, den første er Heap, og den anden er Stack. Lad os gå videre til den sjove del 🤩🍻.

Heap & Stack

Jeg vil ikke gøre den for lang. Lad os komme lige til sagen, en kort beskrivelse, Stack bruges til statisk hukommelsesallokering, mens Heap bruges til dynamisk hukommelsesallokering. Husk blot på, at både Heap og Stack er gemt i RAM.

Mere om Heap-hukommelsen

Java Heap-hukommelse bruges af den virtuelle maskine til at allokere objekter. Når du opretter et objekt, oprettes det altid i heap-hukommelsen. Virtuelle maskiner, som JVM eller DVM, udfører regelmæssig garbage collection (GC), hvilket gør heap-hukommelsen for alle objekter, der ikke længere refereres til, tilgængelig for fremtidige allokeringer.

For at give en problemfri brugeroplevelse sætter Android en hård grænse for heap-størrelsen for hvert kørende program. Grænsen for heap-størrelsen varierer fra enhed til enhed og er baseret på, hvor meget RAM en enhed har. Hvis din app rammer denne heap-grænse og forsøger at allokere mere hukommelse, modtager den en OutOfMemoryError og afsluttes.

Har du nogensinde spekuleret på, hvad heap-størrelsen for din applikation er?

Lad os finde ud af det sammen. I android har vi Dalvik VM (DVM). DVM er en unik Java Virtual Machine, der er optimeret til mobile enheder. Den optimerer den virtuelle maskine med hensyn til hukommelse, batterilevetid og ydeevne, og den er ansvarlig for at fordele mængden af hukommelse til hvert program.

Lad os tale om to linjer i DVM:

  1. dalvik.vm.heapgrowthlimit: Denne linje er baseret på, hvordan Dalvik vil starte i heapstørrelsen for dit program. Det er standardheapstørrelsen for hvert program. Den maksimale størrelse, som din applikation kan nå!
  2. dalvik.vm.heapsize: Denne linje repræsenterer den maksimale heap-størrelse for en større heap. Det kan du opnå ved at bede android om en større heap i dit programmanifest (android:largeHeap=”true”).

Du må ikke bruge en større heap i din app. Gør det KUN, hvis du kender den nøjagtige bivirkning af dette trin. Her vil jeg give dig nok oplysninger til at fortsætte med at undersøge emnet.

Her er en tabel, der viser, hvilken heap-størrelse du fik baseret på din enheds RAM:

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

Husk, jo mere ram du har, jo højere vil heap-størrelsen være. Husk, at det ikke er alle enheder med højere ram, der har mere end 512m, men undersøg din enhed, hvis din enhed har mere end 3 GB, for at se, om din heap-størrelse er større end 512m.

Hvordan kan du kontrollere app-heap-størrelsen for din enhed?

Hvis du bruger ActivityManager. Du kan kontrollere den maksimale heap-størrelse ved kørselstid ved at bruge metoderne getMemoryClass() eller getLargeMemoryClass() (når en stor heap er aktiveret).

  • getMemoryClass():
  • getLargeMemoryClass(): Returnerer den maksimale heap-størrelse som standard.
  • getLargeMemoryClass():
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Hvordan fungerer det i den virkelige verden?

Vi vil bruge dette enkle program til at forstå, hvornår vi bruger heap’en, og hvornår vi bruger stakken.

applikation

Billedet nedenfor viser en repræsentation af applikationens heap og stack, og hvor hvert objekt peger og gemmes, når vi kører app’en.

Vi vil gennemgå udførelsen af appen, stoppe hver linje, forklare, hvornår applikationen allokerer objekterne og gemmer dem i heap’en eller stakken. Vi vil også se, hvornår appen frigiver objekterne fra stakken og heap’en.

  • Linje 1 – JVM’en opretter en stack-hukommelsesblok til hovedmetoden.

  • Linje 2 – I denne linje opretter vi en primitiv lokal variabel. Variablen vil blive oprettet og gemt i stakhukommelsen i hovedmetoden.

  • Linje 3 -Her har jeg brug for din opmærksomhed!!! I denne linje opretter vi et nyt objekt. Objektet oprettes i stakken i main-metoden og gemmes i heap’en. Stakken gemmer referencen, objektet-heap-hukommelsesadressen (pointer), mens heap’en gemmer det oprindelige objekt.

  • Linje 4 – Det samme som linje 3.

  • Linje 5 – JVM’en opretter en stackhukommelsesblok for foo-metoden.

  • Linje 6 -Opret et nyt objekt. Objektet oprettes i stakhukommelsen i foo-metoden, og vi gemmer i stakken heap-hukommelsesadressen for det objekt, vi oprettede i linje 3. Værdien (heap-hukommelsesadressen for objektet i linje 3), som vi videregav i linje 5. Husk på, at Java altid videregiver referencer ved værdi.

  • Linje 7 – Vi opretter et nyt objekt. Objektet oprettet i stakken og pegede på strengpuljen i heap’en.

  • Linje 8 – I den sidste linje i foo-metoden afsluttedes metoden. Og objekterne frigives fra stakblokken i foo-metoden.

  • Linje 9- Det samme som linje 8, I den sidste linje i main-metoden, metoden termineret. Og stakblokken i hovedmetoden bliver fri.

Hvad med frigørelse af hukommelsen fra heap’en? Det kommer vi snart til. Snup en kaffe☕️, og fortsæt 😼.

Hvad sker der, når metoder termineres?

Hver metode har sit eget scope. Når metoden afsluttes, frigives og genindvindes objekterne automatisk fra stakken.

Figur 1

I figur 1, når foo-metoden afsluttes. Stakhukommelsen eller stakblokken i foo-metoden frigives og genindvindes automatisk.

figur 2

I figur 2, Det samme. Når main-metoden afsluttes. Stakhukommelsen eller stakblokken i hovedmetoden frigives og genindvindes automatisk.

Slutning

Nu er det klart for os, at objekterne i stakken er midlertidige. Når metoden afsluttes, vil objekterne blive frigivet og genindvundet.

Stakken er en LIFO-datastruktur (Last-In-First-Out). Du kan se den som en kasse. Ved at bruge denne struktur kan programmet nemt styre alle sine operationer ved hjælp af to enkle operationer: push og pop.

Hver gang du skal gemme noget, f.eks. en variabel eller en metode, skubbes og flyttes stack-pointeren op. Hver gang du forlader en metode, popper den alt fra stack-pointeren, indtil den vender tilbage til adressen på den forrige metode. I vores eksempel vender man tilbage fra foo-metoden til main-metoden.

Hvad med heap’en?

Heap’en er forskellig fra stakken. For at frigive og genvinde objekter fra heap-hukommelsen har vi brug for hjælp.

Dertil har Java, eller for at være mere præcis, JVM’en lavet en superhelt til at hjælpe os. Vi har kaldt den Garbage Collector. Han vil gøre det hårde arbejde for os. Og bekymre sig om at opdage ubrugte objekter, frigive dem og genvinde mere plads i hukommelsen.

Hvordan fungerer garbage collector?

Entydigt. Garbage collector leder efter ubrugte eller uopnåelige objekter. Når der er et objekt i heap’en, som der ikke er nogen reference peget på, sørger garbage collector for at frigøre det fra hukommelsen og genvinde mere plads.

GC

GC-rødder er objekter, der refereres af JVM’en. De er de indledende objekter i træet. Hvert objekt i træet har et eller flere rodobjekter. Så længe programmet eller GC-rødderne kan nå disse rødder eller disse objekter, kan hele træet nås. Når de bliver utilgængelige for programmet eller GC-rødderne, betragtes de som ubrugte objekter eller utilgængelige objekter.

Hvad sker der, når garbage collector kører?

For nu er dette programmets aktuelle hukommelsestilstand. Stakken er tom, og heap’en er fuld af ubrugte objekter.

For GC

Efter kørsel af GC’en vil resultaterne være følgende:

Efter GC

GC frigiver og rydder alle de ubrugte objekter fra heap’en.

Man! Hvad med den hukommelseslækage, som vi venter på? LOL, bare en lille smule mere, og så er vi der. I vores simple applikation blev appen skrevet fantastisk og simpelt. Der var ikke noget galt med koden, der kan forhindre GC i at frigive heapens objekter. Og derfor frigiver GC alle objekter fra heap-hukommelsen og kræver dem tilbage. Fortsæt. Vi har en masse eksempler på hukommelseslækager i det næste afsnit😃.

Hvornår og hvordan opstår hukommelseslækager?

Et hukommelseslækage sker, når stakken stadig refererer til ubrugte objekter i heap’en.

Der er en simpel visuel repræsentation i nedenstående billede for en bedre forståelse af begrebet.

I den visuelle repræsentation kan vi se, at når vi har objekter, der refereres fra stakken, men som ikke længere er i brug. Garbage Collector vil aldrig frigive eller frigøre dem fra hukommelsen, fordi det viser, at disse objekter er i brug, mens de ikke er det.

Hvordan kan vi forårsage en lækage?

Der er forskellige måder at forårsage en hukommelseslækage i Android på. Og det kan gøres nemt ved hjælp af AsyncTasks, Handlers, Singleton, Threads og mere.

Jeg vil vise nogle eksempler ved hjælp af tråde, singleton og lyttere for at forklare, hvordan vi kan forårsage en lækage og undgå og rette dem.

Kig på mit Github-repository. Jeg har nogle kodeeksempler.

Hvordan kan vi forårsage en lækage ved hjælp af tråd?

I dette eksempel vil vi starte en aktivitet, der kører en tråd i baggrunden. Tråden skal udføre en opgave, der tager 20 sekunder at afslutte.

Som bekendt holder de indre klasser en implicit reference til deres omsluttende klasse.

Bag kulisserne ser aktiviteten faktisk sådan her ud.

The DownloadTask holder en reference til ThreadActivity.

Så hvad sker der efter start af opgaven eller tråden?

Der er to mulige flows for brug af applikationen. Et normalt flow, der fungerer som forventet uden fejl, og et leak flow, der forårsager en hukommelseslækage.

Regulært flow

På billedet præsenterer vi applikationens heap og stack.

Brugeren starter programmet, åbner ThreadActivity og ventede på skærmen, indtil downloadopgaven er afsluttet. Brugeren ventede i en 20-sekunder. Hvorfor 20-sekunder? Fordi det er den tid, det tager tråden at afslutte opgaven.

Task Running

Opgaven kører i baggrunden. Brugeren ventede i 20 sekunder på, at downloadopgaven blev afsluttet. Når opgaven er færdig, frigiver stakken blokken run()-metoden.

Der er ingen reference, der indeholder DownloadTask. GC betragtede DownladTask-objektet som et ubrugt objekt, og derfor vil den næste GC-cyklus rydde det fra heap-hukommelsen.

GC rydder de ubrugte objekter fra heap-hukommelsen. Når brugeren nu lukker aktiviteten. Hovedmetoden frigives fra stakken, og i den næste GC-cyklus vil GC rydde ThreadActivity fra heap-hukommelsen.

Perfekt!