Allt du behöver veta om minnesläckor i Android.

Ali Asadi
Jun 30, 2019 – 16 min read

En av de centrala fördelarna med Java, eller för att vara mer exakt, av JVM (Java Virtual Machine), är skräpplockaren (Garbage Collector, GC). Vi kan skapa nya objekt utan att behöva oroa oss för att frigöra dem från minnet. Garbage Collector tar hand om att allokera och frigöra minnet åt oss.

Inte precis! Vi kan hindra garbage collector från att frigöra minnet åt oss om vi inte förstår helt och hållet hur GC fungerar.

Skriva en kod utan att förstå hur GC fungerar kan ge upphov till minnesläckor i appen. Dessa läckor kan påverka vår app genom att slösa bort outnyttjat minne och så småningom orsaka out of memory-exceptioner och förseningar.

Vad är minnesläckage?

Svikt på att frigöra oanvända objekt från minnet

Svikt på att frigöra oanvända objekt från minnet innebär att det finns oanvända objekt i applikationen som GC inte kan rensa från minnet.

När GC inte kan rensa de oanvända objekten från minnet är vi illa ute. Den minnesenhet som innehåller de oanvända objekten kommer att vara upptagen till slutet av programmet eller (till slutet av metoden).

Till slutet av metoden? Ja, det stämmer. Vi har två typer av läckor, läckor som upptar minnesenheten fram till slutet av programmet och läckor som upptar minnesenheten fram till slutet av metoden. Den första är tydlig. Den andra behöver förtydligas ytterligare. Låt oss ta ett exempel för att förklara detta! Anta att vi har metod X. Metod X utför någon långkörningsuppgift i bakgrunden, och det kommer att ta en minut att avsluta den. Dessutom håller metod X oanvända objekt medan den gör detta. I det fallet kommer minnesenheten att vara upptagen och de oanvända objekten kan inte rensas under en minut förrän uppgiften är avslutad. Efter metodens avslutande kan GC rensa de oanvända objekten och återta minnet.

Det är vad jag vill att ni ska veta för tillfället vi kommer att återkomma till detta senare med lite kod och visualisering. DET KOMMER ATT BLI ROLIGT. 👹😜

Vänta lite!!🥴

För att hoppa till sammanhanget börjar vi med grunderna.

RAM, eller Random access memory, är det minne i androidenheter eller datorer som används för att lagra aktuella körda program och deras data.

Jag kommer att förklara två huvudpersoner i RAM, den första är Heap och den andra är Stack. Låt oss gå vidare till den roliga delen 🤩🍻.

Heap & Stack

Jag tänker inte göra den för lång. Låt oss gå rakt på sak, en kort beskrivning: Stack används för statisk minnesallokering medan Heap används för dynamisk minnesallokering. Tänk bara på att både Heap och Stack lagras i RAM.

Mer om Heap-minne

Java Heap-minne används av den virtuella maskinen för att allokera objekt. När du skapar ett objekt skapas det alltid i heap-minnet. Virtuella maskiner, som JVM eller DVM, utför regelbunden garbage collection (GC), vilket gör heapminnet för alla objekt som inte längre refereras till tillgängligt för framtida allokeringar.

För att ge en smidig användarupplevelse sätter Android en hård gräns för heapstorleken för varje kört program. Gränsen för heapstorlek varierar mellan olika enheter och baseras på hur mycket RAM en enhet har. Om din app når denna heap-gräns och försöker allokera mer minne får den en OutOfMemoryError och avslutas.

Har du någonsin undrat vad heap-storleken för din applikation är?

Låt oss upptäcka detta tillsammans. I android har vi Dalvik VM (DVM). DVM är en unik virtuell Java-maskin som är optimerad för mobila enheter. Den optimerar den virtuella maskinen för minne, batteritid och prestanda och ansvarar för att fördela mängden minne för varje applikation.

Låt oss prata om två rader i DVM:

  1. dalvik.vm.heapgrowthlimit: Den här raden baseras på hur Dalvik kommer att börja i heapstorleken för din applikation. Det är standardstorleken för heapstorleken för varje program. Det maximala som din applikation kan nå!
  2. dalvik.vm.heapsize: Den här raden representerar den maximala heapstorleken för en större heap. Det kan du uppnå genom att be android om en större heap i ditt programmanifest (android:largeHeap=”true”).

Använd inte en större heap i din app. Gör det BARA om du vet exakt vilken bieffekt detta steg har. Här kommer jag att ge dig tillräckligt med information för att du ska kunna fortsätta att forska i ämnet.

Här är en tabell som visar vilken heap-storlek du fick baserat på enhetens RAM:

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

Håll dig till minnet att ju mer ram du har, desto större blir heap-storleken. Kom ihåg att det inte är alla enheter med högre ram som har mer än 512m, gör din undersökning om din enhet har mer än 3GB för att se om din heap-storlek är större än 512m.

Hur kan du kontrollera appens heap-storlek för din enhet?

Användning av ActivityManager. Du kan kontrollera den maximala heapstorleken vid körning genom att använda metoderna getMemoryClass() eller getLargeMemoryClass() (när en stor heap är aktiverad).

  • getMemoryClass():
  • getLargeMemoryClass(): Återger standardvärdet för den maximala heapstorleken.
  • getLargeMemoryClass(): Återger den maximala heapstorleken:
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Hur fungerar det i verkligheten?

Vi kommer att använda det här enkla programmet för att förstå när vi använder heap och när vi använder stacken.

applikation

Bilden nedan visar en representation av applikationens heap och stack och var varje objekt pekar och lagras när vi kör applikationen.

Vi kommer att gå igenom appens exekvering, stanna varje rad, förklara när applikationen allokerar objekten och lagrar dem i heap eller stack. Vi kommer också att se när appen släpper objekten från stacken och heap.

  • Linje 1 – JVM:en skapar ett minnesblock i stacken för huvudmetoden.

  • Linje 2 – I den här raden skapar vi en primitiv lokal variabel. Variabeln kommer att skapas och lagras i huvudmetodens stapelminne.

  • Linje 3 -Här behöver jag er uppmärksamhet!!! I den här raden skapar vi ett nytt objekt. Objektet skapas i stacken i huvudmetoden och lagras i heap. Stacken lagrar referensen, minnesadressen (pointer) mellan objekt och heap, medan heap lagrar det ursprungliga objektet.

  • Linje 4 – Samma sak som linje 3.

  • Linje 5 – JVM skapar ett stackminnesblock för metoden foo.

  • Rad 6 -Skapa ett nytt objekt. Objektet skapas i stackminnet för metoden foo, och vi lagrar i stacken heapminnesadressen för det objekt vi skapade i rad 3. Värdet (heapminnesadressen för objektet i rad 3) som vi överlämnade i rad 5. Kom ihåg att Java alltid överlämnar referenser genom värde.

  • Linje 7 – Vi skapar ett nytt objekt. Objektet skapas i stacken och pekar på strängpoolen i heap.

  • Linje 8 – I den sista raden i metoden foo avslutas metoden. Och objekten kommer att släppas från stackblocket i foo-metoden.

  • Linje 9- Samma sak som i linje 8, I den sista linjen i main-metoden, avslutades metoden. Och stackblocket för huvudmetoden blir fritt.

Hur är det med att frigöra minnet från heap? Vi kommer att komma dit snart. Ta en kaffe☕️, och fortsätt 😼.

Vad händer när metoder avslutas?

Varje metod har sitt eget scope. När metoden avslutas släpps objekten och återtas automatiskt från stapeln.

Figur 1

I figur 1, när metoden foo avslutas. Stackminnet eller stackblocket i foo-metoden frigörs och återkrävs automatiskt.

figur 2

I figur 2, Samma sak. När metoden main avslutades. Stackminnet eller stackblocket i huvudmetoden frigörs och återkrävs automatiskt.

Slutsats

Nu står det klart för oss att objekten i stapeln är tillfälliga. När metoden avslutas kommer objekten att släppas och återkrävas.

Stacken är en LIFO-datastruktur (Last-In-First-Out). Du kan se den som en låda. Genom att använda denna struktur kan programmet enkelt hantera alla sina operationer med hjälp av två enkla operationer: push och pop.

Varje gång du behöver spara något, t.ex. en variabel eller en metod, trycker och flyttar den stackpekaren uppåt. Varje gång du avslutar en metod, poppar den upp allt från stackpekaren tills du återvänder till adressen för den föregående metoden. I vårt exempel återvänder man från foo-metoden till huvudmetoden.

Hur är det med heap?

Heap skiljer sig från stacken. För att frigöra och återkräva objekten från heapminnet behöver vi hjälp.

För detta har Java, eller för att vara mer exakt, JVM gjort en superhjälte för att hjälpa oss. Vi kallar den Garbage Collector. Han kommer att göra det svåra arbetet åt oss. Och bry sig om att upptäcka oanvända objekt, släppa dem och återta mer utrymme i minnet.

Hur fungerar garbage collector?

Enkel. Garbage collector letar efter oanvända eller oåtkomliga objekt. När det finns ett objekt i heap som det inte pekas någon referens till tar garbage collector hand om att släppa det från minnet och återkräva mer utrymme.

GC

GC-rötter är objekt som refereras av JVM. De är de första objekten i trädet. Varje objekt i trädet har ett eller flera rotobjekt. Så länge programmet eller GC-rötterna kan nå dessa rötter eller dessa objekt är hela trädet nåbart. När de blir ouppnåeliga från programmet eller GC-rötterna betraktas de som oanvända objekt eller ouppnåeliga objekt.

Vad händer när skräpplockaren körs?

För tillfället är detta programmets aktuella minnestillstånd. Stacken är rensad och högen är full av oanvända objekt.

För GC

Efter att ha kört GC blir resultatet följande:

Efter GC

GC kommer att frigöra och rensa alla oanvända objekt från heap.

Man! Hur blir det med minnesläckan som vi väntar på? LOL, bara en liten bit till så är vi framme. I vårt enkla program var programmet skrivet bra och enkelt. Det var inget fel på koden som kan hindra GC från att släppa heapens objekt. Och av den anledningen släpper GC och återkräver alla objekt från heapminnet. Fortsätt. Vi har många exempel på minnesläckor i nästa avsnitt😃.

När och hur uppstår minnesläckor?

En minnesläcka uppstår när stapeln fortfarande refererar till oanvända objekt i heap.

Det finns en enkel visuell representation i bilden nedan för en bättre förståelse av begreppet.

I den visuella representationen ser vi att när vi har objekt som refereras från stapeln men som inte längre används. Garbage Collector kommer aldrig att släppa eller frigöra dem från minnet eftersom det visar att dessa objekt används medan de inte gör det.

Hur vi kan orsaka en läcka?

Det finns olika sätt att orsaka en minnesläcka i Android. Och det kan göras enkelt med hjälp av AsyncTasks, Handlers, Singleton, Threads med mera.

Jag kommer att visa några exempel med hjälp av trådar, singleton och lyssnare för att förklara hur vi kan orsaka en läcka och undvika och åtgärda dem.

Kolla in mitt Github repository. Jag har några kodexempel.

Hur kan vi orsaka en läcka med hjälp av trådar?

I det här exemplet ska vi starta en aktivitet som kör en tråd i bakgrunden. Tråden kommer att utföra en uppgift som tar 20 sekunder att slutföra.

Som bekant har de inre klasserna en implicit referens till den omslutande klassen.

Här bakom kulisserna är det faktiskt så här aktiviteten ser ut.

The DownloadTask har en referens till ThreadActivity.

Så vad händer efter att ha startat uppgiften eller tråden?

Det finns två möjliga flöden för att använda programmet. Ett vanligt flöde som fungerar som förväntat utan fel och ett läckageflöde som orsakar en minnesläcka.

Reguljärt flöde

I bilden presenterar vi applikationens heap och stack.

Användaren startar programmet, öppnar ThreadActivity och väntar på skärmen tills nedladdningsuppgiften är klar. Användaren väntade i 20 sekunder. Varför 20 sekunder? Därför att det är den tid det tar för tråden att slutföra uppgiften.

Task Running

Uppgiften körs i bakgrunden. Användaren väntade i 20 sekunder på att nedladdningsuppgiften skulle slutföras. När uppgiften är klar släpper stacken metodblocket run().

Det finns ingen referens som håller DownloadTask. GC betraktade DownladTask-objektet som ett oanvänt objekt och av den anledningen kommer nästa GC-cykel att rensa det från heapminnet.

GC rensar de oanvända objekten från heapminnet. När användaren nu stänger aktiviteten. Huvudmetoden kommer att släppas från stacken, och i nästa GC-cykel kommer GC att rensa ThreadActivity från heapminnet.

Perfekt!