Już dostępny

Program Szkoleniowy Java Developer dostępny 🔥💪 tylko TERAZ za 999 zł  Sprawdź szczegóły i agendę

Zakres

Monitoring • Apache Kafka • Clean Code Testowanie • Hibernate • Systemy kolejkowe Sprawdź szczegóły i agendę

Zakres

14 modułów  /  ponad 40h nagrań  /  230 lekcji  /  dożywotni dostęp  /  Sprawdź szczegóły i agendę

Jak działa Garbage Collector – zarządzanie pamięcią JVM

utworzone przez Java

[Szybkie info]: Startujemy z II edycją Programu Szkoleniowego Java Developera 🚀. To MEGA piguła wiedzy o Java 🔥💪

  • 14 tygodniowy program szkoleniowy online,
  • 230 lekcji w formie video (40 godzin materiału)
  • z dożywotnim dostępem
  • Case Studies, masz dostęp do kodu i obrazów Dockerowych
  • zamknięta grupa mentorzy + uczestnicy i webinary na żywo

W agendzie znajdziesz: Mikroserwisy, Systemy kolejnowe, Apache Kafka, Caching, Hibernate/MyBatis/Spring Data, techniki efektywnych Testów kodu, Clean Code i Maven.

Tylko teraz dołączysz z 63% rabatem to 2699 zł 999 zł (+VAT). I nigdy już nie będzie taniej. Poniżej dowiesz się więcej:

Zobacz więcej

A teraz przechodzimy do artykułu:

Powód istnienia odśmiecacza pamięci, jakim jest Garbage Collector jest zaskakująco prosty i oczywisty. Programiści wolą bawić się nowymi frameworkami, niż dbać o zwalnianie pamięci, której potrzebują aplikacje. Pamięć z reguły ma ograniczony rozmiar i nie da się jej rozszerzać w nieskończoność. W językach takich jak C – o zwalnianie pamięci musiał zadbać sam programista. Była to męcząca i żmudna praca. Powstawało wiele błędów wycieków pamięci programu. Twórcy Javy chcieli jak najbardziej uprościć proces programowania i zwolnić nas programistów z obowiązku pilnowania zwalniania nieużywanej już pamięci. Wymyślili Garbage Collector, będącym „odśmiecaczem” nieużywanych już obiektów w pamięci JVM. Znajomość tego, jak działa GC jest kluczowa przy „tuningu” aplikacji.

Przed przeczytaniem niniejszego artykułu zachęcam serdecznie do zapoznania się z artykułami (lepiej zrozumiesz pracę Garbage Collector):

  1. Co każdy programista Java powinien wiedzieć o JVM
  2. Obszary pamięci Maszyny Wirtualnej Javy (JVM)

Czym jest Garbage Collector?

Tytułowy Garbage Collector to nic innego jak aplikacja, która nieustannie dba o pamięć naszej aplikacji. Gdyby nie GC, pamięć naszego programu zapełniłaby się bardzo szybko – uniemożliwiałoby to dalszą pracę programu. Garbage Collector stale monitoruje obiekty naszej aplikacji, kiedy oznaczy je jako już nie potrzebne – zabiera się za czyszczenie pamięci z niepotrzebnych już „śmieciowych” obiektów. Spójrzmy na poniższy przykład.

BigDecimal sum(List<BigDecimal> numbers) {
  BigDecimal result = BigDecimal.ZERO;
  for (BigDecimal number : numbers) {
	result = result.add(number);
  }
  return result;
}

Ponieważ obiekt BigDecimal jest obiektem typu immutable – operacje arytmetyczne na tym obiekcie w rezultacie zwracają nowy obiekt. Powyższa metoda sumująca, dla miliona elementów doda do pamięci co najmniej tyle samo obiektów. Nie jest tak, że te „tymczasowe” obiekty BigDecimal będą cały czas potrzebne. Po skończeniu operacji dodawania – obiekty można usunąć (właśnie tym zajmuje się Garbage Collector). Gdyby nie praca GC – pamięć aplikacji bardzo szybko by się wyczerpała.

Rodzaje algorytmów

Technologia odśmiecania pamięci nie została wynaleziona przez twórców Javy. Została ona opracowana pod koniec lat 50tych, na potrzeby języka LISP. Początkowo technologia odśmiecania pamięci opierała się na algorytmach skalarnych, które są stosunkowo proste, aczkolwiek wykorzystywane do dziś w takich językach jak Python, C++ czy PHP. Dziś, algorytmy, które wykorzystuje Garbage Collector możemy podzielić na dwa zasadnicze typy: skalarne i wektorowe.

Algorytmy skalarne

Idea algorytmów skalarnych polega na skojarzeniu z każdym obiektem licznika, który wskazuje na liczbę odwołań (referencji). Obrazowo algorytm można przedstawić następująco.

Rysunek 1 – zliczanie referencji.

W pierwszym kroku tworzymy dwa obiekty – użytkowników „Basia” i „Kasia”. Użytkowników tych przypisujemy do zmiennych „a” i „b”. Z każdym obiektem powiązany jest licznik. Początkowo licznik referencji wskazuje 1 dla zmiennej „a” i „b”. W drugim kroku modyfikujemy zmienną „b”, tak aby wskazywała na użytkownika „Basia” – czyli zmienną „a”. W konsekwencji licznik referencji dla obiektu „a” wskazuje liczbę 2, a obiekt „b” utracił referencję do wartości „Kasia”, gdyż wskazuje teraz na obiekt „Basia”. Licznik referencji obiektu „b” pokazuje więc 0. Skoro my nie widzimy referencji obiektu „b” do innych obiektów – odśmiecacz pamięci z pewnością też nie będzie jej widział. Pozwala to na usunięcie obiektu „b” przy kolejnej iteracji odśmiecacza pamięci. Jak więc widać idea algorytmów skalarnych jest całkiem prosta, ale czy na pewno niezawodna? Okazuje się, że algorytmy skalarne nie wykrywają cyklicznych referencji. Powstał problem i inżynierowie wymyślili rozwiązanie – algorytmy wektorowe, które traktują obiekty jako grafy, a nie płaskie struktury.

Piguła wiedzy o najlepszych praktykach testowania w Java

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

Algorytmy wektorowe

Algorytmy wektorowe są określane inaczej jako algorytmy śledzące. Idea ich działania polega na umieszczeniu wszystkich obiektów w grafie, przejściu całego grafu i oznaczeniu obiektów, do których udało nam się dotrzeć. Obiekty bez referencji zostaną usunięte podczas pracy Garbage Collectora. Brzmi prosto – gdzie jest haczyk? Jeśli zaczniemy budowanie grafu od nieodpowiedniego obiektu, w skrajnym przypadku może dojść do sytuacji, kiedy oznaczone zostaną obiekty porzucone przez aplikację. Prezentuje to poniższa ilustracja.

Rysunek 1 – błędne oznaczenie korzenia.

W powyższym przykładzie ROOT2 został oznaczony jako zły początek grafu i wskazuje tylko na „object3„. W takim przypadku tracimy obiekty „object1” i „object2„. Początkiem grafu powinien być ROOT1.

Aby nie dopuścić do takiej sytuacji, budowanie grafu zależności, powinniśmy zacząć od obiektów, które na pewno są „żywe”. Obiekty takie nazywamy korzeniami (GC roots). Takim korzeniem może być np. uruchomiony wątek czy zmienna lokalna. Postarajmy się zobrazować proces działania algorytmu wektorowego.

  • Oznaczanie korzeni – to pierwsza czynność, która pozwoli nam na rozpoczęcie budowania grafów. Na poniższej fotografii oznaczono dwa korzenie – ROOT1 i ROOT2.
Rysunek 2 – oznaczanie korzeni.
  • Zaznaczanie obiektów „żywych” – w tym kroku przechodzimy po referencjach obiektów i oznaczamy je jako „żywe”. Czynność ta jest powtarzana dotąd, aż obiekt nie będzie na nic wskazywać. Wówczas dotrzemy do końca gałęzi grafu. Na poniższej fotografii kolorem zielonym oznaczono obiekty „żyjące”.
Rysunek 3 – oznaczenie obiektów żywych.

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.

.

  • Oznaczenie „nieżywych” obiektów – wszystkie pozostałe obiekty, do których nie udało nam się dotrzeć są oznaczane jako „nieżywe”, nie zależnie od tego, czy są z nimi powiązane referencje, czy nie. Z punktu widzenia aplikacj – są bezużyteczne, gdyż nie można się do nich dostać. Na poniższej fotografii, kolorem pomarańczowym oznaczono obiekty „nieżywe”.
Rysunek 4 – oznaczanie obiektów martwych.
  • Zwolnienie pamięci – ostatni etap polegający na usunięciu „martwych” obiektów oraz wyczyszczeniu marketów „żyjących” obiektów, aby przygotować je do kolejnego przebiegu Garbage Collectora. Proces prezentuje poniższa fotografia.
Rysunek 5 – zwalnianie pamięci.
  • Zapętlenie algorytmu – i wszystko od początku.

Sytuacja może się bardziej skomplikować, ponieważ prawidłowo musimy wyznaczyć punkty startowe dla algorytmu poszukującego. Kolejnym problemem jest dynamizm samej aplikacji, która podczas swojej pracy, tworzy nowe obiekty, wątki itp. Najprostszym rozwiązaniem jest zatrzymanie aplikacji, podczas pracy Garbage Collectora – nie będzie ona wtedy tworzyć nowych obiektów. Ten przestój jest potocznie nazywany pauzami Garbage Collectora. Twórcy JVM lwią część swoich wysiłków poświęcają na optymalizację algorytmów czyszczących pamięć, aby skrócić czas przestoju aplikacji do minimum. Jednym z pierwszych rozwiązań problemu jest wdrożenie tzw. hipotezy generacyjnej.

Hipoteza generacyjna

Twórcy JVM zauważyli, że większość obiektów szybko staje się bezużyteczna z punktu widzenia aplikacji. Kolejną obserwacją jest to, że odwołania starych obiektów do nowych są rzadkością (przykład z sumowaniem BigDecimal). Można tu przytoczyć „hipotezę żółwią”, czyli większość młodych (obiektów), tuż po urodzeniu umrze (będzie bezużyteczna). Te żółwie (obiekty), którym uda się dotrzeć z plaży do morza zwiększają swoje szanse na przeżycie kilkuset-krotnie (po pewnym czasie, obiekty można uznać za przydatne). Nasuwa się wniosek, aby pamięć, gdzie składowane są obiekty podzielić na generację młodą i starą, gdzie w kolejnych iteracjach odśmiecania pamięci Garbage Collector Będzie przenosił obiekty z generacji młodej (te które „przeżyły”) do generacji starej. Podział można zobrazować na rysunku poniżej. Więcej o strukturze pamięci JVM pisałem w tym artykule.

Jak widzimy w generacji Young mamy dodatkowy podział na Eden, Survivor 0 i Survivor 1. W wielkim skrócie jest to rozbicie pamięci, które ma na celu jeszcze bardziej zmniejszyć przestoje Garbage Collector. W tym momencie nie będę się dalej rozwodził na temat pamięci JVM – opisałem ją szczegółowo w tym artykule.

Pamięć HEAP – Algorytm kopiujący

Ilość algorytmów GC nie zna końca :). Algorytm kopiujący jest jednym z ważniejszych algorytmów Garbage Collectora. Wkracza on do pracy po skończonym procesie oznaczania żywych i martwych obiektów. Wymaga on dwa razy więcej pamięci, niż ta, która realnie byłaby wykorzystana przez obiekty. Za cenę zwiększonego zużycia pamięci, otrzymujemy ciągłą przestrzeń, niepodlegającą fragmentacji. Tak jak wspominałem wcześniej, obiekty są przenoszone z młodszych generacji pamięci do tych starszych. Można wysnuć wniosek, że obiekty z generacji Eden podlegają największej fragmentacji, ponieważ zmiany wśród młodych obiektów zachodzą częściej niż wśród starych obiektów, należących do generacji Old. Technicznie wygląda to w ten sposób:

  1. W pierwszym przebiegu GC oznacza obiekty żywe (w generacji Eden) i przenosi je do generacji wyżej, czyli Survivor 0. Następnie elementy nieoznaczone jako żywe – usuwa.
  2. Podobna operacja jest robiona na generacji Survivor 0. A mianowicie – GC ponownie oznacza żywotność obiektów. Te, które są oznaczone jako żywe – przenosi do kolejnej generacji, czyli Survivor 1. Pozostałe obiekty zostają usunięte.
  3. W kolejnym przebiegu Garbage Collectora sprawdzana jest generacja Survivor 1. I ponownie – obiekty żywe dostają promocję do generacji starej Old, obiekty martwe są usuwane.
  4. I tak w kółko. Jeśli jakiemuś obiektowi uda się „przetrwać” – docelowo jest on kopiowany do generacji Old. Młodsze generacje pamięci służą jako przestrzeń „przetrwalnikowa” dla obiektów naszej aplikacji.

Dzięki takiemu podziałowi pamięci jeszcze bardziej minimalizujemy przestoje Garbage Collectora, a co więcej, na każdej generacji pamięci jesteśmy w stanie zastosować inny algorytm oczyszczający pamięć.

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.

.

Typy Garbage Collector

W Garbage Collector możemy wyróżnić cztery główne typy, w zależności od trybu ich wykonywania. Każdy typ GC ma swoje plusy i minusy. Najważniejsze jest to, że my jako programiści jesteśmy w stanie świadomie wybrać odpowiedni rodzaj GC, który będzie używany przez nasz JVM. Każdy z typów GC może zapewnić różną wydajność aplikacji. Bardzo ważne jest zrozumienie zasady działania konkretnych typów Garbage Collectora – tak aby świadomie wybrać ten odpowiedni, w zależności od warónków działania aplikacji.

Serial

Najprostszy typ. Wykonywanie wątków aplikacji jest zawieszone (czarne strzałki), a jedynym wątkiem mającym dostęp do pamięci jest wątek Garbage Collectora (kolor czerwony).

Rysunek 7 – algorytmy typu Serial

Algorytm ten jest wykorzystywany w środowiskach jednowątkowych. Sposób działania, polegający na zamrożeniu wszystkich wątków aplikacji podczas czyszczenia pamięci może nie być odpowiedni dla środowiska serwerowego. Serial najlepiej nadaje się do prostych programów wiersza poleceń.

Aby użyć szeregowego modułu odśmiecania pamięci, włącz argument -XX: + UseSerialGC w JVM.

Parallel

Działa podobnie jak w przypadku Serial, z tą różnicą, że wątków Garbage Collectora jest więcej.

Rysunek 8 – algorytmy typu Parallel.

Moduł Parallel jest domyślnym modułem odśmiecania pamięci w JVM.

Concurrent Mark Sweep (CMS)

CMS używa wielu wątków do skanowania pamięci sterty w celu oznaczenia obiektów do usunięcia, a następnie je usuwa.

Rysunek 9 – algorytm CMS.

CMS GC „używa” wątków aplikacji tylko w dwóch scenariuszach:

  1. Podczas oznaczania obiektów, do których istnieją odniesienia w pamięci.
  2. Jeśli równolegle nastąpi zmiana w pamięci sterty podczas czyszczenia pamięci.

W porównaniu z równoległym odśmiecaniem pamięci, moduł CMS GC zużywa więcej procesora, aby zapewnić lepszą przepustowość aplikacji. CMS należy używać wtedy, gdy jesteśmy w stanie przydzielić więcej mocy procesora, aby zapewnić lepszą wydajność.

Aby użyć CMS, ustaw w JVM opcję: -XX: + USeParNewGC

G1

Ten GC jest używany w przypadku dużych obszarów pamięci sterty. Algorytm dzieli stertę na regiony i zaczyna pracować równolegle.

Rysunek 10 – algorytm typu G1.

Aby użyć modułu G1, włącz w JVM opcję: –XX: + UseG1GC.

W poniższej tabeli podsumowałem typy Garbage Collectorów i opcje ich włączenia w JVM.

W poniższej tabeli zaprezentowane są opcje GC, dzięki którym możemy zoptymalizować cały JVM.

Przykład użycia omawianych opcji.

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar

W następnych artykułach rozprawimy się m.in. z tuningiem JVM, wajchami, na które trzeba uważać na produkcji, czy badaniem wycieków pamięci. 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

Podsumowanie

W niniejszym artykule omówiliśmy szereg algorytmów, które zarządzają pamięcią. Nie są to oczywiście wszystkie algorytmy Garbage Collectora (jest ich kilkadziesiąt). Często nieznajomość istnienia podziału pamięci JVM i sposobu pracy Garbage Collectora powoduje błędy przy „tuningu” Maszyny Wirtualnej Javy. Równie często błędnie analizowane są raporty z testów wydajnościowych, kiedy to pod uwagę brany jest czas, gdy aplikacja „rozgrzewa się”. Mam nadzieję, że choć trochę przybliżyłem Ci zasadę działania Garbage Collectora. W następnym artykule omówimy, w jaki sposób tuningować i monitorować Maszynę Wirtualną Javy.

Zdjęcie: Biznes plik wektorowy utworzone przez jemastock – pl.freepik.com

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 👌

Piguła wiedzy o najlepszych praktykach testowania w Java

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

Dyskusja