CodeGym /Kursy /JAVA 25 SELF /Garbage collectory: G1, ZGC, Shenandoah, porównanie

Garbage collectory: G1, ZGC, Shenandoah, porównanie

JAVA 25 SELF
Poziom 64 , Lekcja 1
Dostępny

1. Wprowadzenie do garbage collectora (GC)

Jeśli kiedykolwiek pisałeś w C lub C++, to zapewne zetknąłeś się z koniecznością ręcznego zwalniania pamięci przy pomocy free() lub delete. W Javie jest znacznie prościej: tworzysz obiekt przez new, a usuwać go nie musisz — tym zajmuje się specjalny „sprzątacz” zwany garbage collectorem (Garbage Collector, GC).

GC to część JVM, która automatycznie zwalnia pamięć zajętą przez obiekty, do których nie ma już referencji. Dzięki temu programiści Java nie muszą martwić się, że zapomną zwolnić pamięć (i doprowadzą do wycieku), albo przeciwnie — przypadkowo usuną obiekt, który jest nadal potrzebny (co skończy się crashem).

Ale, jak każdy „sprzątacz”, GC nie jest idealny: czasem może wkroczyć w najmniej odpowiednim momencie, urządzając „generalne sprzątanie” (Stop-the-World), albo działać wolniej, niż byśmy chcieli. Dlatego w JVM istnieje kilka różnych implementacji garbage collectora — a wybór właściwej może zauważalnie wpłynąć na wydajność aplikacji.

Podstawowe typy garbage collectorów

Serial GC

  • Serial GC — najprostszy i najstarszy garbage collector.
  • Działa w jednym wątku.
  • Zatrzymuje wszystkie pozostałe wątki na czas sprzątania (Stop-the-World).
  • Dobry dla małych aplikacji bez intensywnej wielowątkowości.
  • Włączany flagą: -XX:+UseSerialGC

Parallel GC

  • Parallel GC (zwany też „Throughput Collector”).
  • Używa wielu wątków do sprzątania.
  • Zorientowany na maksymalną przepustowość.
  • Nadal wykonuje sprzątanie z pauzami Stop-the-World, ale szybciej niż Serial.
  • Pasuje do aplikacji serwerowych, w których niewielkie pauzy nie są krytyczne.
  • Włączany flagą: -XX:+UseParallelGC

CMS (Concurrent Mark Sweep)

  • CMS — przestarzały, lecz długo popularny GC minimalizujący pauzy.
  • Działa częściowo równolegle z aplikacją, skracając czas zatrzymania.
  • Bardziej złożony w konfiguracji, ma narzuty.
  • Od Java 9 oznaczony jako deprecated.
  • Włączany flagą: -XX:+UseConcMarkSweepGC

G1 (Garbage First)

  • G1 GC — nowoczesny collector domyślny (od Java 9).
  • Równoważy minimalne pauzy i wydajność.
  • Dzieli stertę na wiele małych regionów (model regionalny).
  • Może zbierać śmieci wybiórczo, regionami, bez dotykania całej sterty.
  • Pozwala ustawić docelową maksymalną pauzę, np. -XX:MaxGCPauseMillis=200.
  • Flaga włączenia: -XX:+UseG1GC (zwykle niepotrzebna, bo G1 jest domyślny).

ZGC i Shenandoah

  • ZGC i Shenandoah — nowoczesne garbage collectory o niskich opóźnieniach (low‑latency).
  • Cel — minimalne pauzy (milisekundy), nawet przy ogromnych stertach (do terabajtów).
  • Działają niemal całkowicie równolegle z aplikacją.
  • Wymagają Java 11+ (ZGC) lub Java 12+ (Shenandoah).
  • Odpowiednie dla systemów wrażliwych na opóźnienia (giełdy, fintech, analityka czasu rzeczywistego).
  • Flagi włączenia: -XX:+UseZGC lub -XX:+UseShenandoahGC

3. Zasady działania nowoczesnych GC

Młode i stare pokolenie (Young/Old Generation)

JVM dzieli stertę na dwie duże części:

Młode pokolenie (Young Generation): tu trafiają wszystkie nowe obiekty. Sprzątanie odbywa się tu często i szybko (Minor GC).

Stare pokolenie (Old Generation, Tenured): tu „przeprowadzają się” obiekty, które przetrwały kilka sprzątań w młodym pokoleniu. Sprzątanie jest rzadsze, ale dłuższe (Major/Full GC).

Dlaczego tak? Większość obiektów w Javie żyje bardzo krótko (np. tymczasowe łańcuchy, kolekcje wewnątrz metody). Dlatego młode pokolenie można sprzątać szybko i często, nie dotykając starego.

Minor GC

  • Czyści tylko młode pokolenie.
  • Szybki, z krótką pauzą.
  • Nie dotyka starych obiektów.

Major (Full) GC

  • Czyści całą stertę (zarówno młode, jak i stare pokolenie).
  • Może zajmować dużo czasu (sekundy i więcej przy dużych stertach).
  • Zwykle towarzyszy mu długa pauza aplikacji.

Jak GC określa, które obiekty usunąć?

GC szuka „żywych” obiektów, zaczynając od referencji korzeniowych (root set): zmienne lokalne na stosach wątków, pola statyczne, parametry metod itd. Wszystko, do czego można „dosięgnąć”, uznaje się za żywe. Reszta to śmieci.

4. Porównanie nowoczesnych garbage collectorów: G1, ZGC, Shenandoah

Przyjrzyjmy się, czym różnią się najnowocześniejsze i najpopularniejsze GC. Oto tabela poglądowa:

Kolektor Cel główny Model pamięci Minimalne pauzy Skalowalność Wsparcie Kiedy używać
G1 Balans pauz/wydajności Regiony ~10–200 ms Do setek GB Java 9+ (domyślnie) Większość aplikacji serwerowych
ZGC Minimalna pauza Regiony, „kolorowe znaczniki” <10 ms Do terabajtów Java 11+ Czas rzeczywisty, wrażliwe na opóźnienia (latency‑critical)
Shenandoah Minimalna pauza Regiony, „kolorowe znaczniki” <10 ms Do terabajtów Java 12+ (Red Hat) Czas rzeczywisty, wrażliwe na opóźnienia (latency‑critical)

G1 GC: Garbage First

  • Dzieli stertę na wiele regionów (zwykle 1–32 MB każdy).
  • Podczas sprzątania wybiera regiony z największą ilością śmieci („garbage first”).
  • Może zbierać tylko część sterty, a nie całość naraz.
  • Pozwala ustawić docelową pauzę: -XX:MaxGCPauseMillis=200.
  • Dobry kompromis między szybkością a pauzami; używany domyślnie od Java 9.

Przykład włączenia (gdyby był wyłączony):

java -XX:+UseG1GC -jar myapp.jar

ZGC: Z Garbage Collector

  • Eksperymentalny w Java 11, stabilny od Java 15.
  • Praktycznie nie zatrzymuje aplikacji: pauzy zwykle <10 ms, nawet przy 1–2 TB sterty.
  • Używa „kolorowych znaczników” (coloring) i specjalnych wskaźników.
  • Wymaga 64‑bitowej JVM; nie działa na systemach 32‑bitowych.
  • Obsługiwany na Linux, macOS, Windows.

Przykład włączenia:

java -XX:+UseZGC -jar myapp.jar

Shenandoah

  • Opracowany przez Red Hat; cele podobne do ZGC.
  • Minimalne pauzy, aktywna praca równolegle z aplikacją.
  • Wsparcie dla Linux i Windows; część wydań OpenJDK.
  • Stosuje podobne techniki, ale inne wewnętrzne algorytmy.

Przykład włączenia:

java -XX:+UseShenandoahGC -jar myapp.jar

Porównanie wizualne

graph TD
    A[Młode pokolenie] -->|Minor GC| B[Stare pokolenie]
    B -->|Major GC| C[Pauza GC]
    D[G1: regiony] --> E[Wybrane regiony]
    F[ZGC/Shenandoah: regiony] --> G[Równoległe sprzątanie]

5. Praktyka: jak sprawdzić i zmienić GC

Jak sprawdzić, który GC jest używany?

  1. Logi JVM: Uruchom aplikację z parametrami -Xlog:gc* (Java 9+) lub -verbose:gc (do Java 8). W logach będzie widać, który GC jest używany i jak często występują pauzy.
  2. jcmd: Wykonaj:
    jcmd <pid> VM.flags
    
    gdzie <pid> — identyfikator procesu Javy.
  3. jvisualvm: W sekcji „Monitorowanie” można sprawdzić typ GC.

Jak zmienić GC dla swojej aplikacji?

Dodaj odpowiednią flagę przy uruchamianiu programu Java:

G1 GC (domyślny, można wskazać jawnie):

java -XX:+UseG1GC -jar myapp.jar

ZGC:

java -XX:+UseZGC -jar myapp.jar

Shenandoah:

java -XX:+UseShenandoahGC -jar myapp.jar

Jak ustawić rozmiar sterty i pauzy?

  • Maksymalny rozmiar sterty: -Xmx2G
  • Minimalny rozmiar sterty: -Xms512M
  • Dla G1: docelowa pauza — -XX:MaxGCPauseMillis=200

Przykład pełnego uruchomienia:

java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar

6. Dobór GC do różnych zadań

Kiedy wybrać G1

  • W większości aplikacji serwerowych i desktopowych — świetny wybór domyślny.
  • Dobrze działa przy stertach od setek megabajtów do setek gigabajtów.
  • Równoważy szybkość i pauzy.

Kiedy wybrać ZGC lub Shenandoah

  • Jeśli aplikacja jest wrażliwa na opóźnienia (latency‑critical: giełdy, gry online, analityka czasu rzeczywistego).
  • Jeśli sterta jest ogromna (setki gigabajtów i więcej).
  • Jeśli dopuszczalne są tylko minimalne pauzy (milisekundy).
  • Wymagane Java 11+ (ZGC) lub Java 12+ (Shenandoah).

Kiedy wystarczy Parallel GC

  • Dla małych aplikacji, gdzie liczy się maksymalna przepustowość, a pauzy nie są krytyczne.
  • Dla przetwarzania wsadowego (batch), gdzie można „przetrwać” zatrzymanie na Full GC.

7. Przykład: porównanie zachowania GC na prostej aplikacji

Niewielka aplikacja generująca dużo obiektów tymczasowych (symulacja przetwarzania zamówień):

public class GCSimulator {
    public static void main(String[] args) {
        while (true) {
            // Tworzymy 100 000 obiektów w każdej iteracji
            for (int i = 0; i < 100_000; i++) {
                String s = new String("Order-" + i);
            }
            // Krótka przerwa
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
    }
}

Uruchom ją z różnymi GC i przyjrzyj się logom:

java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator

Co zobaczysz?
G1 będzie wykonywać częste, ale krótkie pauzy. ZGC/Shenandoah — jeszcze krótsze pauzy, ale mogą występować częściej. Parallel GC — dłuższe, lecz rzadsze pauzy.

8. Typowe błędy i niuanse przy pracy z GC

Błąd nr 1: Oczekiwanie, że GC rozwiąże wszystkie problemy z pamięcią. GC to nie magiczna różdżka. Jeśli trzymasz referencje do niepotrzebnych obiektów, żaden GC nie pomoże — dojdzie do wycieku pamięci.

Błąd nr 2: Wymuszone wywołanie System.gc(). JVM najlepiej wie, kiedy sprzątać. Wymuszony GC może wywołać długą pauzę i obniżyć wydajność.

Błąd nr 3: Ignorowanie logów GC. Jeśli nie patrzysz w logi GC, możesz nie zauważyć, że Twoja aplikacja regularnie „zawiesza się” na Full GC.

Błąd nr 4: Używanie przestarzałych GC. Na przykład CMS nie jest już rozwijany. Lepiej przejść na G1 lub nowoczesne collectory o niskich opóźnieniach.

Błąd nr 5: Nieprawidłowy wybór GC do zadania. Jeśli masz aplikację wrażliwą na opóźnienia (latency‑critical), a używasz Parallel GC — licz się z długimi pauzami. Jeśli masz przetwarzanie wsadowe (batch), a włączysz ZGC — poniesiesz zbędne narzuty.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION