Wszystko, co musisz wiedzieć o wyciekach pamięci w systemie Android.

Ali Asadi
Jun 30, 2019 – 16 min read

Jedną z podstawowych zalet Javy, a dokładniej mówiąc, JVM (Java Virtual Machine), jest garbage collector (GC). Możemy tworzyć nowe obiekty nie martwiąc się o zwalnianie ich z pamięci. Garbage collector zajmie się przydzielaniem i zwalnianiem pamięci za nas.

Nie do końca! Możemy zapobiec zwolnieniu pamięci przez garbage collector, jeśli nie do końca rozumiemy, jak działa GC.

Pisanie kodu bez dobrego zrozumienia, jak działa GC, może spowodować wycieki pamięci w aplikacji. Te wycieki mogą wpłynąć na naszą aplikację poprzez marnowanie nie zwolnionej pamięci i ostatecznie spowodować wyjątki braku pamięci i lagi.

Co to jest Memory Leak?

Nieudane zwalnianie nieużywanych obiektów z pamięci

Nieudane zwalnianie nieużywanych obiektów z pamięci oznacza, że w aplikacji znajdują się nieużywane obiekty, których GC nie może wyczyścić z pamięci.

Gdy GC nie może wyczyścić nieużywanych obiektów z pamięci, mamy kłopoty. Jednostka pamięci, która przechowuje nieużywane obiekty, będzie zajęta do końca aplikacji lub (do końca metody).

Do końca metody? Tak, to prawda. Mamy dwa rodzaje przecieków, przecieki, które zajmują jednostkę pamięci do końca aplikacji i przecieki, które zajmują jednostkę pamięci do końca metody. Pierwszy z nich jest jasny. Drugi wymaga więcej wyjaśnień. Weźmy przykład, aby to wyjaśnić! Załóżmy, że mamy metodę X. Metoda X wykonuje w tle jakieś długo trwające zadanie, a jego zakończenie zajmie minutę. Ponadto, metoda X trzyma nieużywane obiekty podczas wykonywania tego zadania. W takim przypadku jednostka pamięci będzie zajęta, a nieużywane obiekty nie będą mogły być wyczyszczone przez minutę, aż do zakończenia zadania. Po zakończeniu metody, GC może wyczyścić nieużywane obiekty i odzyskać pamięć.

To jest to, co chcę, abyś wiedział na razie wrócimy do tego później z pewnym kodem i wizualizacją. BĘDZIE FAJNIE. 👹😜

Wait a minute!!!🥴

Zanim przeskoczymy do kontekstu, zacznijmy od podstaw.

RAM, czyli Random access memory, to pamięć w urządzeniach android lub komputerach, która służyła do przechowywania aktualnie działających aplikacji i ich danych.

Mam zamiar wyjaśnić dwa główne znaki w pamięci RAM, pierwszy to Heap, a drugi to Stack. Przejdźmy do części rozrywkowej 🤩🍻.

Heap & Stack

Nie będę się zbytnio rozwodził. Przejdźmy od razu do rzeczy, krótki opis, Stack jest używany do statycznej alokacji pamięci, podczas gdy Heap jest używany do dynamicznej alokacji pamięci. Należy pamiętać, że zarówno sterta jak i stos są przechowywane w pamięci RAM.

Więcej o pamięci sterty

Pamięć sterty w Javie jest używana przez maszynę wirtualną do przydzielania obiektów. Kiedykolwiek tworzysz obiekt, jest on zawsze tworzony w stercie. Maszyny wirtualne, takie jak JVM lub DVM, wykonują regularne zbieranie śmieci (GC), udostępniając pamięć sterty wszystkich obiektów, które nie są już przywoływane, dla przyszłych alokacji.

Aby zapewnić płynne doświadczenie użytkownika, Android ustawia twardy limit na rozmiar sterty dla każdej uruchomionej aplikacji. Limit rozmiaru sterty różni się w zależności od urządzenia i jest oparty na tym, ile pamięci RAM ma urządzenie. Jeśli twoja aplikacja trafi na ten limit sterty i spróbuje przydzielić więcej pamięci, otrzyma OutOfMemoryError i zakończy działanie.

Czy kiedykolwiek zastanawiałeś się, jaki jest rozmiar sterty dla twojej aplikacji?

Odkryjmy to razem. W androidzie mamy maszynę wirtualną Dalvik VM (DVM). DVM jest unikalną maszyną wirtualną Java zoptymalizowaną dla urządzeń mobilnych. Optymalizuje maszynę wirtualną pod kątem pamięci, czasu pracy na baterii i wydajności, a także odpowiada za dystrybucję ilości pamięci dla każdej aplikacji.

Porozmawiajmy o dwóch liniach w DVM:

  1. dalvik.vm.heapgrowthlimit: Ta linia jest oparta na tym, jak Dalvik rozpocznie się w rozmiarze sterty twojej aplikacji. Jest to domyślny rozmiar sterty dla każdej aplikacji. Maksymalny, że Twoja aplikacja może osiągnąć!
  2. dalvik.vm.heapsize: Ta linia reprezentuje maksymalny rozmiar sterty dla większej sterty. Możesz to osiągnąć, prosząc androida o większą stertę w swoim manifeście aplikacji (android:largeHeap=”true”).

Nie używaj większej sterty w swojej aplikacji. Zrób to TYLKO wtedy, gdy znasz dokładnie efekt uboczny tego kroku. Tutaj dam ci wystarczająco dużo informacji, aby kontynuować badania nad tematem.

Tutaj jest tabela pokazująca jaki rozmiar sterty dostałeś w oparciu o pamięć RAM twojego urządzenia:

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

Pamiętaj im więcej ram masz tym większy będzie rozmiar sterty. pamiętaj, że nie wszystkie urządzenia z wyższą pamięcią RAM przekraczają 512m, wykonaj badania na swoim urządzeniu, jeśli twoje urządzenie ma więcej niż 3GB, aby sprawdzić, czy rozmiar sterty jest większy niż 512m.

Jak możesz sprawdzić rozmiar sterty aplikacji dla swojego urządzenia?

Używając ActivityManager. Możesz sprawdzić maksymalny rozmiar sterty w czasie uruchamiania, używając metod getMemoryClass() lub getLargeMemoryClass() (gdy włączona jest duża sterta).

  • getMemoryClass(): Zwróć domyślny maksymalny rozmiar sterty.
  • getLargeMemoryClass(): Zwróć maksymalny dostępny rozmiar sterty po włączeniu flagi large heap w manifeście.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Jak to działa w prawdziwym świecie?

Użyjemy tej prostej aplikacji, aby zrozumieć, kiedy używamy sterty, a kiedy stosu.

aplikacja

Następny obrazek przedstawia reprezentację sterty i stosu aplikacji oraz gdzie każdy obiekt wskazuje i jest przechowywany, gdy uruchamiamy aplikację.

Przejdziemy przez wykonanie aplikacji, zatrzymamy każdą linię, wyjaśnimy kiedy aplikacja alokuje obiekty i przechowuje je na stercie lub stosie. Zobaczymy również, kiedy aplikacja zwalnia obiekty ze stosu i sterty.

  • Linia 1 – JVM tworzy blok pamięci stosu dla metody main.

  • Linia 2 – W tej linii tworzymy prymitywną zmienną lokalną. Zmienna będzie tworzona i przechowywana w pamięci stosu metody głównej.

  • Linia 3 -Tutaj potrzebuję Twojej uwagi!!! W tej linii tworzymy nowy obiekt. Obiekt ten jest tworzony na stosie metody main i przechowywany na stercie. Stos przechowuje referencję, adres pamięci obiekt- sterta (pointer), natomiast sterta przechowuje oryginalny obiekt.

  • Linia 4 – To samo co linia 3.

  • Linia 5 – JVM tworzy blok pamięci stosu dla metody foo.

  • Linia 6 -Utworzenie nowego obiektu. Obiekt jest tworzony w pamięci stosu metody foo, a na stosie przechowujemy adres pamięci sterty obiektu, który utworzyliśmy w Linii 3. Wartość (adres pamięci sterty obiektu w linii 3) przekazaliśmy w linii 5. Należy pamiętać, że Java zawsze przekazuje referencje przez wartość.

  • Linia 7 – Tworzymy nowy obiekt. Obiekt tworzony na stosie i wskazywany na pulę stringów w stercie.

  • Linia 8 – W ostatniej linii metody foo następuje zakończenie metody. A obiekty zostaną zwolnione z bloku stosu metody foo.

  • Linia 9- To samo co linia 8, W ostatniej linii metody main, metoda zakończona. A blok stosu metody main staje się wolny.

Co ze zwalnianiem pamięci ze sterty? Zaraz się tym zajmiemy. Weź kawę☕️, i kontynuuj 😼.

Co się dzieje, gdy metody się kończą?

Każda metoda ma swój własny zakres. Kiedy metoda zostaje zakończona, obiekty są zwalniane i odzyskiwane automatycznie ze stosu.

figura 1

Na rysunku 1, kiedy metoda foo została zakończona. Pamięć stosu lub blok stosu metody foo zostaną zwolnione i odzyskane automatycznie.

figura 2

Na rysunku 2, To samo. Kiedy metoda main została zakończona. Pamięć stosu lub blok stosu głównej metody zostanie zwolniony i odzyskany automatycznie.

Wniosek

Teraz jest to dla nas jasne, że obiekty na stosie są tymczasowe. Po zakończeniu działania metody, obiekty zostaną zwolnione i odzyskane.

Stos jest strukturą danych LIFO (Last-In-First-Out). Można ją postrzegać jako pudełko. Dzięki użyciu tej struktury program może łatwo zarządzać wszystkimi swoimi operacjami za pomocą dwóch prostych operacji: push i pop.

Za każdym razem, gdy trzeba coś zapisać, np. zmienną lub metodę, to wypycha i przesuwa wskaźnik stosu w górę. Za każdym razem, gdy wychodzisz z metody, wyskakuje wszystko ze wskaźnika stosu, aż do powrotu pod adres poprzedniej metody. W naszym przykładzie powrót z metody foo do metody main.

Co ze stertą?

Sterta różni się od stosu. Do zwalniania i odzyskiwania obiektów z pamięci sterty potrzebujemy pomocy.

Do tego Java, a dokładniej JVM stworzyła superbohatera, który ma nam pomóc. Nazwaliśmy go Garbage Collector. Będzie on wykonywał za nas ciężką pracę. I dbając o wykrywanie nieużywanych obiektów, zwalnia je i odzyskuje więcej miejsca w pamięci.

Jak działa garbage collector?

Proste. Garbage collector szuka nieużywanych lub nieosiągalnych obiektów. Gdy na stercie znajduje się obiekt, który nie ma wskazanej żadnej referencji do niego, garbage collector zajmie się zwolnieniem go z pamięci i odzyskaniem większej ilości miejsca.

GC

Korzenie GC są obiektami, do których odwołuje się JVM. Są one początkowymi obiektami drzewa. Każdy obiekt w drzewie ma jeden lub więcej obiektów korzeni. Tak długo, jak aplikacja lub korzenie GC mogą dotrzeć do tych korzeni lub tych obiektów, całe drzewo jest osiągalne. Gdy staną się one nieosiągalne dla aplikacji lub korzeni GC, zostaną uznane za obiekty nieużywane lub nieosiągalne.

Co się stanie, gdy zadziała garbage collector?

Na razie jest to bieżący stan pamięci aplikacji. Stos jest czysty, a sterta jest pełna nieużywanych obiektów.

Przed GC

Po uruchomieniu GC wyniki będą następujące:

Po GC

GC GC zwolni i wyczyści wszystkie nieużywane obiekty ze sterty.

Człowieku! A co z wyciekiem pamięci, na który czekamy? LOL, jeszcze tylko trochę i będziemy na miejscu. W naszej prostej aplikacji, aplikacja została napisana świetnie i prosto. Nie było nic złego w kodzie, który może uniemożliwić GC zwolnienie obiektów sterty. I z tego powodu GC zwalnia i odzyskuje wszystkie obiekty z pamięci sterty. Kontynuuj. Mamy wiele przykładów przecieków pamięci w następnej sekcji😃.

Kiedy i jak zdarzają się przecieki pamięci?

Przeciek pamięci zdarza się, gdy stos wciąż odwołuje się do nieużywanych obiektów na stercie.

Na poniższym obrazku znajduje się prosta reprezentacja wizualna dla lepszego zrozumienia koncepcji.

W reprezentacji wizualnej widzimy, że gdy mamy obiekty odwołujące się ze stosu, ale już nie używane. Garbage collector nigdy nie zwolni lub uwolni je z pamięci, ponieważ pokazuje, że te obiekty są w użyciu, podczas gdy nie są.

Jak możemy spowodować wyciek?

Istnieją różne sposoby na spowodowanie wycieku pamięci w Androidzie. I można to zrobić łatwo za pomocą AsyncTasks, Handlers, Singleton, Threads, i więcej.

Pokażę kilka przykładów z wykorzystaniem wątków, singleton, i słuchaczy, aby wyjaśnić, jak możemy spowodować wyciek i uniknąć i naprawić je.

Sprawdź moje repozytorium Github. Mam kilka przykładów kodu.

Jak możemy spowodować wyciek używając wątku?

W tym przykładzie zamierzamy uruchomić aktywność, która uruchamia wątek w tle. Wątek ten będzie wykonywał zadanie, którego ukończenie zajmuje 20 sekund.

Jak wiadomo, klasy wewnętrzne utrzymują niejawne odniesienie do swojej klasy zamykającej.

Za kulisami, tak w rzeczywistości wygląda aktywność.

Klasa DownloadTask trzyma referencję do ThreadActivity.

Co się dzieje po uruchomieniu zadania lub wątku?

Istnieją dwa możliwe sposoby korzystania z aplikacji. Przepływ regularny, który działa zgodnie z oczekiwaniami bez błędów, oraz przepływ nieszczelności, który powoduje wyciek pamięci.

Przepływ regularny

Na rysunku przedstawiamy stertę i stos aplikacji.

Użytkownik uruchomił aplikację, otworzył ThreadActivity i czekał na ekranie do zakończenia zadania pobierania. Użytkownik czekał przez 20 sekund. Dlaczego 20-sekund? Ponieważ jest to czas, jaki wątek potrzebuje na wykonanie zadania.

Task Running

Zadanie działa w tle. Użytkownik czekał przez 20 sekund na zakończenie zadania pobierania plików. Gdy zadanie zostanie wykonane, stos zwalnia blok metody run().

Nie ma referencji przechowującej obiekt DownloadTask. GC uznał obiekt DownladTask za obiekt nieużywany i z tego powodu w następnym cyklu GC wyczyści go z pamięci sterty.

GC wyczyścił nieużywane obiekty ze sterty. Teraz, gdy użytkownik zamknie aktywność. Metoda main zostanie zwolniona ze stosu, a w następnym cyklu GC wyczyści ThreadActivity z pamięci sterty.

Perfect!

.