Wszystko, co musisz wiedzieć o wyciekach pamięci w systemie Android.
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?
- Wait a minute!!!🥴
- Heap & Stack
- Więcej o pamięci sterty
- Czy kiedykolwiek zastanawiałeś się, jaki jest rozmiar sterty dla twojej aplikacji?
- Jak możesz sprawdzić rozmiar sterty aplikacji dla swojego urządzenia?
- Jak to działa w prawdziwym świecie?
- Co się dzieje, gdy metody się kończą?
- Wniosek
- Co ze stertą?
- Jak działa garbage collector?
- Co się stanie, gdy zadziała garbage collector?
- Kiedy i jak zdarzają się przecieki pamięci?
- Jak możemy spowodować wyciek?
- Jak możemy spowodować wyciek używając wątku?
- Przepływ regularny
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
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.
Na rysunku 1, kiedy metoda foo
została zakończona. Pamięć stosu lub blok stosu metody foo zostaną zwolnione i odzyskane automatycznie.
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.
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.
Po uruchomieniu GC wyniki będą następujące:
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.
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!
.