Tot ce trebuie să știți despre Memory Leaks în Android.

Ali Asadi
30 iun 2019 – 16 min citește

Unul dintre avantajele principale ale Java, sau, pentru a fi mai exact, al JVM (Java Virtual Machine), este colectorul de gunoi (GC). Putem crea obiecte noi fără să ne facem griji cu privire la eliberarea lor din memorie. Colectorul de gunoi va avea grijă să aloce și să elibereze memoria pentru noi.

Nu chiar așa! Putem împiedica garbage collector-ul să elibereze memoria pentru noi dacă nu înțelegem pe deplin cum funcționează GC.

Scrierea unui cod fără o bună înțelegere a modului în care funcționează GC ar putea produce scurgeri de memorie în aplicație. Aceste scurgeri pot afecta aplicația noastră prin irosirea memoriei neeliberate și, în cele din urmă, cauzează excepții de lipsă de memorie și întârzieri.

Ce este scurgerea de memorie?

Eșecul de a elibera obiecte nefolosite din memorie

Eșecul de a elibera obiecte nefolosite din memorie înseamnă că există obiecte nefolosite în aplicație pe care GC nu le poate șterge din memorie.

Când GC nu poate șterge obiectele nefolosite din memorie, avem probleme. Unitatea de memorie care deține obiectele nefolosite va fi ocupată până la sfârșitul aplicației sau (până la sfârșitul metodei).

Până la sfârșitul metodei? Da, așa este. Avem două tipuri de scurgeri, scurgeri care ocupă unitatea de memorie până la sfârșitul aplicației și scurgeri care ocupă unitatea de memorie până la sfârșitul metodei. Prima este clară. Cel de-al doilea are nevoie de mai multe clarificări. Să luăm un exemplu pentru a explica acest lucru! Să presupunem că avem metoda X. Metoda X execută o sarcină de cursă lungă în fundal și va dura un minut pentru a se termina. De asemenea, metoda X deține obiecte nefolosite în timp ce face acest lucru. În acest caz, unitatea de memorie va fi ocupată, iar obiectele nefolosite nu pot fi șterse timp de un minut până la terminarea sarcinii. După terminarea metodei, GC poate șterge obiectele nefolosite și poate recupera memoria.

Acesta este ceea ce vreau să știți deocamdată, vom reveni la acest aspect mai târziu cu ceva cod și vizualizare. VA FI DISTRACTIV. 👹😜

Așteptați un minut!!! 🥴

Înainte de a trece la context, să începem cu elementele de bază.

RAM, sau memoria cu acces aleatoriu, este memoria din dispozitivele sau computerele android care este utilizată pentru a stoca aplicațiile care rulează în prezent și datele acestora.

Voi explica două personaje principale din RAM, primul este Heap, iar al doilea este Stack. Să trecem la partea distractivă 🤩🍻.

Heap & Stack

Nu o să o fac prea lungă. Să trecem direct la subiect, o scurtă descriere, Stack-ul este utilizat pentru alocarea statică a memoriei, în timp ce Heap-ul este utilizat pentru alocarea dinamică a memoriei. Rețineți doar că atât Heap-ul cât și Stack-ul sunt stocate în memoria RAM.

Mai multe despre memoria Heap

Memoria Heap Java este folosită de mașina virtuală pentru a aloca obiecte. Ori de câte ori creați un obiect, acesta este întotdeauna creat în heap. Mașinile virtuale, cum ar fi JVM sau DVM, efectuează în mod regulat garbage collection (GC), făcând ca memoria heap a tuturor obiectelor care nu mai sunt referite să fie disponibilă pentru alocări viitoare.

Pentru a oferi o experiență de utilizare fără probleme, Android stabilește o limită dură a dimensiunii heap pentru fiecare aplicație care rulează. Limita dimensiunii heap variază în funcție de dispozitive și se bazează pe cantitatea de memorie RAM de care dispune un dispozitiv. Dacă aplicația dvs. atinge această limită heap și încearcă să aloce mai multă memorie, va primi un OutOfMemoryError și se va încheia.

V-ați întrebat vreodată care este dimensiunea heap pentru aplicația dvs.?

Să descoperim împreună acest lucru. În android, avem Dalvik VM (DVM). DVM este o mașină virtuală Java unică, optimizată pentru dispozitive mobile. Acesta optimizează mașina virtuală pentru memorie, durata de viață a bateriei și performanță și este responsabil pentru distribuirea cantității de memorie pentru fiecare aplicație.

Să vorbim despre două linii din DVM:

  1. dalvik.vm.heapgrowthlimit: Această linie se bazează pe modul în care Dalvik va porni în dimensiunea heap a aplicației dumneavoastră. Este dimensiunea implicită a heap-ului pentru fiecare aplicație. Maximul pe care aplicația dvs. îl poate atinge!
  2. dalvik.vm.heapsize: Această linie reprezintă dimensiunea maximă a heap-ului pentru un heap mai mare. Puteți obține acest lucru cerându-i lui android un heap mai mare în manifestul aplicației dumneavoastră (android:largeHeap=”true”).

Nu folosiți un heap mai mare în aplicația dumneavoastră. Faceți acest lucru NUMAI dacă știți exact efectul secundar al acestui pas. Aici vă voi da suficiente informații pentru a continua cercetarea subiectului.

Iată un tabel care arată ce dimensiune a heap-ului ați obținut în funcție de memoria RAM a dispozitivului:

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

Amintiți-vă că cu cât aveți mai mult RAM, cu atât dimensiunea heap-ului va fi mai mare. rețineți că nu toate dispozitivele cu un RAM mai mare depășesc 512m faceți cercetări pe dispozitivul dvs. dacă dispozitivul dvs. are mai mult de 3GB pentru a vedea dacă dimensiunea heap-ului dvs. este mai mare de 512m.

Cum puteți verifica dimensiunea heap-ului aplicației pentru dispozitivul dvs.

Utilizând ActivityManager. Puteți verifica dimensiunea maximă a heap-ului în timpul execuției utilizând metodele getMemoryClass() sau getLargeMemoryClass() (atunci când este activat un heap mare).

  • getMemoryClass(): Returnează dimensiunea maximă implicită a heap-ului.
  • getLargeMemoryClass(): Returnează dimensiunea maximă disponibilă a heap-ului după activarea indicatorului large heap în manifest.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Cum funcționează în lumea reală?

Vom folosi această aplicație simplă pentru a înțelege când folosim heap-ul și când utilizăm stiva.

aplicație

Imaginea de mai jos arată o reprezentare a heap-ului și a stivei aplicației și unde punctează și se stochează fiecare obiect atunci când rulăm aplicația.

Vom trece în revistă execuția aplicației, vom opri fiecare linie, vom explica momentul în care aplicația alocă obiectele și le stochează în heap sau în stack. Vom vedea, de asemenea, când aplicația eliberează obiectele din stivă și din heap.

  • Linia 1 – JVM-ul creează un bloc de memorie în stivă pentru metoda main.

  • Linia 2 – În această linie, creăm o variabilă locală primitivă. Variabila va fi creată și stocată în memoria stivă a metodei principale.

  • Linia 3 -Aici am nevoie de atenția voastră!!! În această linie, creăm un nou obiect. Obiectul este creat în stiva metodei principale și este stocat în heap. Stiva stochează referința, adresa de memorie obiect-heap (pointer), în timp ce heap-ul stochează obiectul original.

  • Linia 4 – La fel ca și linia 3.

  • Linia 5 – JVM creează un bloc de memorie de stivă pentru metoda foo.

  • Linia 6 -Crearea unui nou obiect. Obiectul este creat în memoria stivă a metodei foo și stocăm în stivă adresa din memoria heap a obiectului pe care l-am creat la linia 3. Valoarea (adresa de memorie heap a obiectului din linia 3) pe care am trecut-o în linia 5. Rețineți că Java transmite întotdeauna referințele prin valoare.

  • Linia 7 – Creăm un nou obiect. Obiectul creat în stivă și îndreptat către string pool din heap.

  • Linia 8 – În ultima linie a metodei foo, metoda s-a încheiat. Și obiectele vor fi eliberate din blocul de stivă al metodei foo.

  • Linia 9- La fel ca linia 8, În linia finală a metodei main, metoda s-a încheiat. Iar blocul de stivă al metodei principale devine liber.

Cum rămâne cu eliberarea memoriei din heap? Vom ajunge acolo în curând. Luați-vă o cafea☕️, și continuați 😼.

Ce se întâmplă când metodele s-au terminat?

Care metodă are propriul domeniu de aplicare. Când metoda se termină, obiectele sunt eliberate și recuperate automat din stivă.

figura 1

În figura 1, când metoda foo s-a terminat. Memoria stivei sau blocul de stivă al metodei foo va fi eliberată și recuperată automat.

figura 2

În figura 2, Același lucru. Când metoda main s-a încheiat. Memoria stivei sau blocul de stivă al metodei principale va fi eliberată și recuperată automat.

Concluzie

Acum, ne este clar că obiectele din stivă sunt temporare. Odată ce metoda se termină, obiectele vor fi eliberate și recuperate.

Stiva este o structură de date LIFO (Last-In-First-Out). O puteți vizualiza ca pe o cutie. Prin utilizarea acestei structuri, programul poate gestiona cu ușurință toate operațiile sale folosind două operații simple: push și pop.

De fiecare dată când trebuie să salvați ceva, cum ar fi o variabilă sau o metodă, aceasta împinge și mută pointerul stivei în sus. De fiecare dată când ieșiți dintr-o metodă, acesta scoate totul din pointerul de stivă până la revenirea la adresa metodei anterioare. În exemplul nostru, revenind de la metoda foo la metoda main.

Ce se întâmplă cu Heap?

Heap este diferit de stivă. Pentru eliberarea și recuperarea obiectelor din memoria heap, avem nevoie de ajutor.

Pentru asta, Java sau, mai exact, JVM-ul a făcut un supererou pentru a ne ajuta. L-am numit Garbage Collector (colector de gunoi). El va face munca grea pentru noi. Și se va îngriji să detecteze obiectele nefolosite, să le elibereze și să recupereze mai mult spațiu în memorie.

Cum funcționează garbage collector?

Simplu. Colectorul de gunoi caută obiecte nefolosite sau inaccesibile. Atunci când există un obiect în heap care nu are nici o referință îndreptată către el, garbage collector se va ocupa de eliberarea lui din memorie și va recupera mai mult spațiu.

GC

Rădăcinile GC sunt obiecte la care face referire JVM-ul. Acestea sunt obiectele inițiale ale arborelui. Fiecare obiect din arbore are unul sau mai multe obiecte rădăcină. Atâta timp cât aplicația sau rădăcinile GC pot ajunge la aceste rădăcini sau la aceste obiecte, întregul arbore este accesibil. Odată ce acestea devin inaccesibile de la aplicație sau de la rădăcinile GC, vor fi considerate obiecte nefolosite sau obiecte inaccesibile.

Ce se întâmplă când se execută garbage collector?

Pentru moment, aceasta este starea de memorie curentă a aplicației. Stiva este liberă, iar heap-ul este plin de obiecte neutilizate.

Înainte de GC

După rularea GC, rezultatele vor fi următoarele:

După GC

CGC va elibera și va șterge toate obiectele neutilizate din heap.

Homenește! Cum rămâne cu scurgerea de memorie pe care o așteptăm? LOL, doar un pic mai mult Lil și vom ajunge acolo. În aplicația noastră simplă, aplicația a fost scrisă grozav și simplu. Nu era nimic în neregulă cu codul care să împiedice GC să elibereze obiectele din heap. Și din acest motiv, GC eliberează și revendică toate obiectele din memoria heap. Continuați. Avem o mulțime de exemple de scurgeri de memorie în secțiunea următoare😃.

Când și cum se produc scurgeri de memorie?

O scurgere de memorie se produce atunci când stiva face încă referințe la obiecte nefolosite din heap.

Există o reprezentare vizuală simplă în imaginea de mai jos pentru o mai bună înțelegere a conceptului.

În reprezentarea vizuală, vedem că atunci când avem obiecte referite din stivă, dar care nu mai sunt folosite. Colectorul de gunoi nu le va elibera niciodată sau nu le va elibera din memorie pentru că arată că acele obiecte sunt în uz în timp ce nu sunt.

Cum putem provoca o scurgere?

Există diverse moduri de a provoca o scurgere de memorie în Android. Și se poate face cu ușurință folosind AsyncTasks, Handlers, Singleton, Threads și multe altele.

Voi arăta câteva exemple folosind thread-uri, singleton și ascultători pentru a explica cum putem provoca o scurgere și pentru a le evita și repara.

Veziți depozitul meu Github. Am câteva exemple de cod.

Cum putem provoca o scurgere folosind fire?

În acest exemplu, vom începe o activitate care rulează un fir în fundal. Firul va efectua o sarcină care durează 20 de secunde pentru a se termina.

După cum se știe, Clasele interioare dețin o referință implicită la clasa care le înconjoară.

În spatele scenei, acesta este de fapt modul în care arată activitatea.

DownloadTask deține o referință la ThreadActivity.

Și ce se întâmplă după pornirea sarcinii sau a thread-ului?

Există două fluxuri posibile pentru utilizarea aplicației. Un flux normal care funcționează conform așteptărilor fără erori și un flux de scurgere care provoacă o scurgere de memorie.

Fluxul normal

În imagine, prezentăm heap-ul și stiva aplicației.

Utilizatorul pornește aplicația, deschide ThreadActivity și a așteptat pe ecran până la terminarea sarcinii de descărcare. Utilizatorul a așteptat timp de 20 de secunde. De ce 20 de secunde? Pentru că acesta este timpul de care are nevoie thread-ul pentru a finaliza sarcina.

Task Running

Taxa se execută în fundal. Utilizatorul a așteptat timp de 20 de secunde finalizarea sarcinii de descărcare. Când sarcina este finalizată, stiva eliberează blocul metodei run().

Nu există nicio referință care să dețină DownloadTask. GC a considerat obiectul DownladTask ca fiind un obiect neutilizat și, din acest motiv, următorul ciclu GC îl va șterge din memoria heap.

CGC șterge obiectele neutilizate din heap. Acum, când utilizatorul închide activitatea. Metoda principală va fi eliberată din stivă, iar în următorul ciclu al GC, GC va șterge ThreadActivity din memoria heap.

Perfect!