Omówienie zarządzania pamięcią

Środowisko wykonawcze Androida (ART) i maszyna wirtualna Dalvik korzystają z przełączania stronmapowania pamięci (mmapping) do zarządzania pamięcią. Oznacza to, że pamięć, którą modyfikuje aplikacja (czy to poprzez przydzielanie nowych obiektów, czy dotykanie mapowanych stron) pozostaje w pamięci RAM i nie może zostać wyrzucona. Jedynym sposobem na zwolnienie pamięci przez aplikację jest zwolnienie odwołań do obiektów, które aplikacja przechowuje, udostępniając pamięć zbieraczowi śmieci. Z jednym wyjątkiem: wszystkie pliki zamapowane bez modyfikacji, takie jak kod, mogą zostać wyrzucone z pamięci RAM, jeśli system chce użyć tej pamięci gdzie indziej.

Na tej stronie wyjaśniamy, jak Android zarządza procesami aplikacji i przydziałem pamięci. Więcej informacji o skuteczniejszym zarządzaniu pamięcią w aplikacji znajdziesz w artykule Zarządzanie pamięcią aplikacji.

Czyszczenie pamięci

Zarządzane środowisko pamięci, takie jak maszyna wirtualna ART lub Dalvik, śledzi każdą alokację pamięci. Gdy stwierdzi, że program nie używa już danego fragmentu pamięci, zwalnia go z powrotem do stosu bez ingerencji programisty. Mechanizm odzyskiwania niewykorzystanej pamięci w ramach zarządzanego środowiska pamięci nosi nazwę zbierania garbage. Zbieranie pamięci podręcznej ma 2 cele: znajdowanie w programie obiektów danych, do których nie będzie można uzyskać dostępu w przyszłości; odzyskiwanie zasobów używanych przez te obiekty.

Kopiec pamięci w Androidzie jest kopiec generacyjny, co oznacza, że śledzi różne zbiory alokacji na podstawie spodziewanego czasu trwania i rozmiaru przydzielonego obiektu. Na przykład ostatnio przydzielone obiekty należą do młodego pokolenia. Jeśli obiekt pozostaje aktywny przez odpowiednio długi czas, może zostać przeniesiony do starszej generacji, a następnie do generacji stałej.

Każde pokolenie stosu ma swój własny górny limit ilości pamięci, jaką mogą zajmować obiekty. Za każdym razem, gdy generacja zaczyna się wypełniać, system wykonuje zdarzenie zbierania pamięci podręcznej w celu zwolnienia pamięci. Czas trwania zbierania garbage zależy od tego, które pokolenie obiektów jest zbierane oraz ile aktywnych obiektów znajduje się w każdym pokoleniu.

Chociaż zbieranie śmieci może być dość szybkie, może to mieć wpływ na wydajność aplikacji. Zwykle nie masz kontroli nad tym, kiedy w Twoim kodzie wystąpi zdarzenie związane z zbieraniem pamięci. System ma zestaw kryteriów, które pozwalają określić, kiedy należy przeprowadzić zbieranie elementów. Gdy kryteria zostaną spełnione, system przerywa wykonywanie procesu i rozpoczyna zbieranie elementów zbędących. Jeśli zbieranie pamięci zbędą odbywać się w trakcie intensywnego przetwarzania, np. animacji lub odtwarzania muzyki, może to wydłużyć czas przetwarzania. Takie zwiększenie może spowodować, że kod w aplikacji będzie uruchamiany dłużej niż zalecany próg 16 ms, który zapewnia wydajne i płynne renderowanie klatek.

Dodatkowo przepływ kodu może wykonywać zadania, które wymuszają częstsze zdarzenia zbierania pamięci podręcznej lub wydłużają ich czas trwania. Jeśli na przykład w najbardziej wewnętrznej części pętli for przydzielisz wiele obiektów w ramach każdego klatki animacji mieszania przezroczystości, możesz zanieczyszczać stos pamięci dużą liczbą obiektów. W takich okolicznościach zbieracz wykonuje wiele zdarzeń zbierania śmieci, co może obniżyć wydajność aplikacji.

Więcej ogólnych informacji o zbieraniu odpadów znajdziesz w artykule Zbieranie odpadów.

Udostępnianie wspomnień

Aby zmieścić wszystko, czego potrzebuje w pamięci RAM, Android próbuje udostępniać strony pamięci RAM w różnych procesach. Możesz to zrobić na kilka sposobów:

  • Każdy proces aplikacji jest pochodnym istniejącego procesu o nazwie Zygote. Proces Zygote uruchamia się, gdy system uruchamia się i wczytuje wspólny kod i zasoby frameworka (np. motywy aktywności). Aby rozpocząć nowy proces aplikacji, system tworzy proces Zygote, a następnie wczytuje i uruchamia kod aplikacji w nowym procesie. Dzięki temu większość stron pamięci RAM przydzielonej do kodu i zasobów platformy może być współużytkowana przez wszystkie procesy aplikacji.
  • Większość danych statycznych jest mapowana na proces. Ta technika umożliwia udostępnianie danych między procesami, a także ich wymianę w razie potrzeby. Przykładowe dane statyczne: kod Dalvik (umieszczony w wstępnie połączonym pliku .odex do bezpośredniego mapowania), zasoby aplikacji (umieszczone w tabeli zasobów jako struktura, która może być mapowana, oraz w uporządkowanych wpisach ZIP pliku APK) oraz tradycyjne elementy projektu, takie jak kod natywny w plikach .so.
  • W wielu miejscach Android udostępnia tę samą dynamiczną pamięć RAM wielu procesom za pomocą wyraźnie przydzielonych regionów współdzielonej pamięci (za pomocą ashmem lub gralloc). Na przykład powierzchnie okien używają wspólnej pamięci między aplikacją a kompozytorem ekranu, a bufory kursora używają wspólnej pamięci między dostawcą treści a klientem.

Ze względu na intensywne korzystanie z pamięci współdzielonej określenie ilości pamięci używanej przez aplikację wymaga zachowania ostrożności. Techniki umożliwiające prawidłowe określenie wykorzystania pamięci przez aplikację omawiamy w artykule Analiza wykorzystania pamięci RAM.

Przydzielanie i odzyskiwanie pamięci aplikacji

Stos Dalvik jest ograniczony do jednego zakresu pamięci wirtualnej dla każdego procesu aplikacji. Określa on rozmiar stosu logicznego, który może się zwiększać w miarę potrzeb, ale tylko do limitu zdefiniowanego przez system w przypadku danej aplikacji.

Rozmiar logiczny stosu nie jest taki sam jak ilość fizycznej pamięci używanej przez stos. Podczas sprawdzania stosu aplikacji Android oblicza wartość zwaną proporcjonalnym rozmiarem zbioru (PSS), która uwzględnia zarówno brudne, jak i czyste strony udostępniane innym procesom, ale tylko w stosunku proporcjonalnym do liczby aplikacji, które korzystają z tej pamięci RAM. Ta łączna wartość (PSS) jest uważana przez system za fizyczny ślad pamięci. Więcej informacji o PSS znajdziesz w poradnikach Analiza wykorzystania pamięci RAM.

Kopiec Dalvik nie kompresuje logicznego rozmiaru kopiec, co oznacza, że Android nie defragmentuje kopiec, aby zwolnić miejsce. Android może zmniejszyć rozmiar logicznego stosu tylko wtedy, gdy na końcu stosu jest niewykorzystane miejsce. System może jednak zmniejszyć ilość pamięci fizycznej używanej przez stos. Po zakończeniu zbierania elementów do wykorzystania Dalvik przeszukuje stos i wykrywa nieużywane strony, a następnie zwraca je do jądra za pomocą madvise. Dlatego parowanie alokacji i zwolnienia dużych fragmentów powinno skutkować odzyskaniem całej (lub prawie całej) pamięci fizycznej. Odzyskiwanie pamięci z mniejszych alokacji może być jednak znacznie mniej wydajne, ponieważ strona używana do małej alokacji może być nadal współdzielona z czymś innym, co nie zostało jeszcze zwolnione.

Ograniczanie pamięci aplikacji

Aby zapewnić sprawne działanie w trybie wielozadaniowym, Android ustawia sztywny limit rozmiaru stosu dla każdej aplikacji. Dokładny limit rozmiaru stosu różni się w zależności od dostępnej pamięci RAM na urządzeniu. Jeśli Twoja aplikacja osiągnie pojemność stosu i próbuje przydzielić więcej pamięci, może otrzymać błąd OutOfMemoryError.

W niektórych przypadkach możesz wysłać zapytanie do systemu, aby dowiedzieć się, ile dokładnie pamięci stosu masz dostępnej na bieżącym urządzeniu. Możesz to zrobić na przykład po to, aby określić, ile danych można bezpiecznie przechowywać w pamięci podręcznej. Aby uzyskać tę wartość, wyślij do systemu zapytanie, dzwoniąc pod numer getMemoryClass(). Ta metoda zwraca liczbę całkowitą wskazującą liczbę megabajtów dostępnych dla stosu aplikacji.

Przełączanie aplikacji

Gdy użytkownicy przełączają się między aplikacjami, Android przechowuje aplikacje, które nie są na pierwszym planie, czyli nie są widoczne dla użytkownika lub nie uruchamiają usługi na pierwszym planie, np. odtwarzania muzyki, w pamięci podręcznej. Na przykład gdy użytkownik uruchamia aplikację po raz pierwszy, zostaje utworzony proces, ale gdy użytkownik ją zamknie, proces ten nie zostanie zamknięty. System przechowuje proces w pamięci podręcznej. Jeśli użytkownik wróci później do aplikacji, system ponownie wykorzysta ten proces, dzięki czemu przełączanie się między aplikacjami będzie szybsze.

Jeśli aplikacja ma proces z pamięcią podręcznej i zachowuje zasoby, których obecnie nie potrzebuje, to nawet wtedy, gdy użytkownik jej nie używa, wpływa na ogólną wydajność systemu. Gdy systemowi brakuje zasobów, takich jak pamięć, zabija procesy w pamięci podręcznej. System uwzględnia też procesy, które zajmują najwięcej pamięci, i może je zakończyć, aby zwolnić pamięć RAM.

Uwaga: im mniej pamięci zużywa aplikacja w pamięci podręcznej, tym większe ma szanse na to, że nie zostanie zamknięta i będzie mogła szybko wznowić działanie. Jednak w zależności od bieżących wymagań systemu procesy buforowane mogą zostać zakończone w dowolnym momencie, niezależnie od wykorzystania zasobów.

Więcej informacji o tym, jak procesy są przechowywane w pamięci podręcznej, gdy nie działają na pierwszym planie, oraz jak Android decyduje, które z nich można zakończyć, znajdziesz w przewodniku Procesy i wątki.

Test obciążeniowy pamięci

Chociaż problemy z obciążeniem pamięci są rzadsze na urządzeniach wyższej klasy, mogą nadal występować w przypadku użytkowników urządzeń z małą ilością pamięci RAM, np. z Androidem (wersja Go). Warto spróbować odtworzyć to środowisko, aby móc napisać testy narzędzia do pomiaru wydajności, które pozwolą sprawdzić zachowanie aplikacji i ulepszyć wrażenia użytkowników na urządzeniach z małą ilością pamięci.

Test obciążeniowy aplikacji

Test obciążeniowy aplikacji (stressapptest) to test interfejsu pamięci, który pomaga tworzyć realistyczne sytuacje obciążenia, aby testować różne ograniczenia pamięci i sprzętu w aplikacji. Dzięki możliwości definiowania ograniczeń czasu i pamięci możesz pisać instrumentację, aby weryfikować rzeczywiste sytuacje obciążenia pamięci. Aby na przykład przesłać stałą bibliotekę do systemu plików danych, sprawić, by była uruchamialna, i przeprowadzić test obciążeniowy przez 20 sekund z 990 MB, użyj tego zestawu poleceń:
    adb push stressapptest /data/local/tmp/
    adb shell chmod 777 /data/local/tmp/stressapptest
    adb shell /data/local/tmp/stressapptest -s 20 -M 990

  

Więcej informacji o instalowaniu narzędzia, typowych argumentach i obsługiwaniu błędów znajdziesz w dokumentacji stressapptest.

Obserwacje dotyczące testu stresowego

Za pomocą narzędzi takich jak stressapptest można żądać przydziału pamięci większej niż dostępna. Takie żądanie może wywołać różne alerty, o których warto wiedzieć ze strony programistów. Trzy główne alerty, które mogą być wyświetlane z powodu niskiej dostępności pamięci:
  • SIGABRT: to krytyczne, natywne zawieszenie procesu spowodowane żądaniem przydzielenia pamięci o większym rozmiarze niż wolna pamięć, podczas gdy system jest już pod presją pamięci.
  • SIGQUIT: generuje zrzut pamięci jądra i kończy proces, gdy zostanie wykryty przez testowanie instrumentacji.
  • TRIM_MEMORY_EVENTS: te funkcje zwracania wywołania są dostępne w Androidzie 4.1 (poziom interfejsu API 16) i nowszych. Zapewniają szczegółowe alerty dotyczące pamięci dla procesu.