Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

Obszary pamięci Maszyny Wirtualnej Javy (JVM)

utworzone przez Java

Pamięć w Javie to temat bardzo często pomijany. Zazwyczaj do tematu wracamy, kiedy aplikacja wyrzuci nam wyjątek: OutOfMemoryError. Ograniczamy się wówczas do ustawienia dwóch podstawowych parametrów JVM, czyli – Xmx (maksymalny Heap) oraz -Xms (startowa wielkość Heap). Niestety zapominamy, że przyczyn pojawienia się wyjątku może być sporo. Warto jest więc poznać, w jaki sposób JVM organizuje pamięć i w jaki sposób ją zarządza.

Z tego artykułu dowiesz się

  • Jaki jest model pamięci JVM
  • Jak JVM zarządza pamięcią
  • Co to jest Heap
  • Co to jest Off-heap
  • Jakie są generacje pamięci JVM

Jeśli nie czytałeś/aś poprzedniego artykułu, mówiącego o tym, czym jest i jak działa Maszyna Wirtualna Javy (JVM) oraz Just In Time Compiller (JIT) – zachęcam Cię do zapoznania się z tym artykułem (niektóre tematy mogą być niejasne, bez wiedzy na temat JVM i JIT)

Cała pamięć, jaką wykorzystuje Maszyna Wirtualna Javy (JVM) dzieli się na kilka poziomów. Podstawowy podział to podział na stertę (heap) i pamięć natywną (off-heap). W wielkim skrócie mogę powiedzieć, że pierwszy poziom (sterta) leży w gestii zainteresowania odśmiecacza pamięci (Garbage Collecotr) a drugi nie. W warstwie sterty żyją obiekty naszej aplikacji. Natomiast w warstwie pamięci natywnej lokowane są głównie byty potrzebne do wsparcia działania samej Wirtualnej Maszyny i reprezentacji jej struktur wewnętrznych. Przyjrzyjmy się bardziej szczegółowo poszczególnym poziomom pamięci JVM.

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Pamięć sterty (HEAP)

Pamięć sterty jest głównym przedmiotem zainteresowania programistów. W tym miejscu żyją wszystkie stworzone przez nas obiekty. Na wydajność naszej aplikacji największy wpływ ma sposób wykorzystania pamięci sterty. Poniższa fotografia prezentuje strukturę pamięci heap.

Pamięć sterty nie jest strukturą ciągłą, podlega dalszemu podziałowi na dwie generacje Young (młodą) i Old (starą). Podział ten powstał na podstawie tzw. hipotezy generacyjności, potocznie nazywanej „śmiertelnością niemowląt”. Z grubsza chodzi o to, że prawdopodobieństwo zguby młodych (niedawno utworzonych) obiektów jest dużo wyższe niż prawdopodobieństwo zguby obiektów starych. Porównując to do żółwi – tuż po wykluciu się, sporo młodych nie przeżyje. Prawdopodobieństwo przeżycia młodych, które dotarły z plaży do wody, gwałtownie wzrasta, a długość życia żółwi wynosi wówczas kilkanaście lat. Bardzo podobnym tokiem rozumowania kierowali się autorzy Javy, dzieląc pamięć na dwie generacje – młodą i starą. Dzięki takiemu podziałowi można zastosować różne algorytmy odśmiecacza pamięci (Garbage Collector) dla konkretnej przynależności obiektu do generacji.

Young & Old

To jednak nie koniec podziału. Generacja Young dzieli się na Eden, Survivor 0, Survivor 1 i Reserved. O co chodzi? Otóż chodzi o algorytmy Garbage Collector, który to odśmieca pamięć z nieużywanych obiektów. W językach takich jak C – sami musieliśmy zadbać o zwalnianie pamięci, żeby nie powodować jej wycieków. W Javie tematem odśmiecania pamięci zajmuje się Garbage Collector (omówimy go w następnym artykule). Jak wiemy, rzadko kiedy mamy taką sytuację, że wszystkie obiekty naszej aplikacji tworzone są na starcie. Mamy dynamiczne listy, dynamiczne obiekty itp. Jeśli Garbage Collector chciałby przeczyścić pamięć, musiałby zastopować wykonywanie aplikacji i przejrzeć pamięć, którą wykorzystuje aplikacja, w celu oznaczenia, które obiekty są używane i mają powiązania do innych, a które nie. Nie da się tego zrobić przy jednolitej strukturze pamięci bez przestoju aplikacji (w trakcie życia aplikacji, co chwile tworzone są nowe obiekty). Może zdarzyć się taka sytuacja, że na konkretny wycinek czasu, dany obiekt nie ma powiązań do innych i może zostać oznaczony jako „do usunięcia” – jednak podczas pracy aplikacji, dowiązanie takie może powstać. Jak się domyślamy usunięcie obiektu, którego za chwile aplikacja będzie potrzebować, spowoduje błędną pracę systemu. Aby rozwiązać ten problem, twórcy Javy podzielili generację Young na pod generacje. I tak na starcie aplikacji, wszystkie nowopowstałe obiekty znajdują się w generacji Eden. W pierwszej iteracji Garbage Collector oznacza obiekty, które nie są używane – właśnie w generacji Eden. Te, które nie są używane – usuwa. Te, których aplikacja używa – przenosi do przetrwalnikowej części pamięci – Survivor 0. Z kolei, obiekty, które nie są używane w gengeneracji Survivor 0 – są usuwane, a te które „przeżyły” są przenoszone do kolejnej generacji przetrwalnikowej – Survivor 1. I znowu, po kolejnej iteracji – Garbage Collector sprawdza przydatność obiektów z generacji Survivor 1 – te obiekty, które dalej są używane, można już uznać za „dorosłe, wylęgnięte” i przenieść je do generacji Old i tam pozostaną do końca swych dni. Natomiast jeśli jakieś obiekty nie są już używane w generacji Survivor 1 – zostaną po prostu usunięte. Cały algorytm jest powtarzany w kółko.

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

W każdej z dwóch dostępnych generacji mamy fragmenty pamięci, które są zarezerwowane, jednak nie wchodzą w skład wykorzystywanych obszarów. Powodem tego jest dynamika struktury pamięci, która w celu osiągnięcia maksymalnej wydajności nieustannie zwiększa lub zmniejsza wielkości poszczególnych segmentów.

Pamięć natywna (OFF HEAP)

O ile sterta jest obszarem pamięci bliższym programisty (świadomie lub nie), o tyle pozostałe obszary pamięci wykorzystuje głównie JVM. W Javie istnieją techniki pozwalające na bezpośredni dostęp do pamięci natywnej, jednak zagadnienia te zdecydowanie wykraczają poza zakres niniejszego artykułu. Struktury natywne zostały wprowadzone w specyfikacji Java 8. Obszar pamięci natywnej off-cheap możemy podzielić na dwie grupy.

Pierwszą grupę stanowią obszary pamięci współdzielonej. Druga grupa to obszary autonomicznej pamięci – dla każdego wątku działającego na naszej maszynie. Jedną z kluczowych zmian, jakie zaszły w zmianie specyfikacji JVM, jest zastąpienie przestrzeni PermGen przez Metaspace. Sama rola Metaspace została zbliżona, jednak implementacja zmieniła się diametralnie.

Metaspace

Przestrzeń Metaspace możemy traktować jako „magazyn” obiektów wewnętrznych Maszyny Wirtualnej Javy. W tym obszarze pamięci przetrzymywane są klasy, metody, wykorzystywane pola (Field & Method Data). Wszystkie referencje, literały znakowe czy stałe również stanowią obszar pamięci Metaspace, a konkretniej jego wyspecjalizowany fragment – Run-time Constant Pool. Ostatnia grupa znajdująca się w Metaspace to Code, czyli kody klas składowanych przez JVM.

Code cache

To kolejny istotny segment pamięci off-cheap. To przestrzeń kluczowa z punktu widzenia optymalizatora kodu Just-In-Time Compiler (JIT, szczegółowo omówiłem w tym artykule). To właśnie tutaj przechowywane są zoptymalizowane i skompilowane kody aplikacji.

Native Area

Pamięć natywna stanowi ostatni współdzielony obszar pamięci. Przy pomocy dość zaawansowanych technik, programista może uzyskać dostęp do tej pamięci. Ma to zastosowanie, kiedy w naszej aplikacji występują np. wyjątkowo duże tablice – wówczas możemy zarządzać nimi samodzielnie, właśnie z tej przestrzeni. Pozwoli to zdjąć ciężar zwalniania zasobów z Garbage Collectora, dla którego pamięć natywna leży daleko poza kręgiem zainteresowania.

Thread

Pozostaje do omówienia część pamięci, która jest wykorzystywana przez poszczególne wątki, żyjące w Maszynie Wirtualnej Javy. Każdy z tych wątków wykorzystuje dwa segmenty:

  • Program counter – zawiera referencję do aktualnie wykonywanej instrukcji kodu. Referencja wskazuje na odpowiedni fragment w przestrzeni MethodArea
  • Stos – to nic innego jak lista LIFO (Last In First Out) zawierająca ramki wywołań. Każda z ramek zawiera m.in. tabelę zmiennych lokalnych oraz wartość zwracaną.

Podsumowanie

Złożoność budowy pamięci JVM jest bardzo duża. Nie sposób omówić wszystkiego w jednym artykule. Skomplikowana budowa pamięci wynika z faktu stosowania różnych algorytmów odśmiecacza pamięci (Garbage Collector). Pamięć została podzielona na segmenty po to, aby nie wprowadzać długich przestojów podczas pracy Garbage Collectora.

W następnych artykułach rozprawimy się m.in. ze sposobem pracy Garbage Collector, który niezawodnie odśmieca pamięć Javy czy badaniem wycieków pamięci na produkcji. Jeśli nie chcesz, aby ominęły Cię tak mięsne artykuły – dołącz do naszego newslettera, a wyślemy Ci powiadomienie, kiedy materiały się pojawią.

Jeśli przeczytałeś/aś ten artykuł i jeśli Ci się spodobał – będę bardzo wdzięczny, jeśli podzielisz się nim ze swoimi znajomymi i dołączysz do naszego fanpage na FaceBook – https://www.facebook.com/softwareskill

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Gdybyś potrzebował jeszcze więcej:

Jesteś Java Developerem?

Przejdź na wyższy poziom wiedzy 
„Droga do Seniora” 🔥💪

Jesteś Team Leaderem? Masz zespół?

Podnieś efektywność i wiedzę swojego zespołu 👌

Dyskusja