ZGC vs G1GC dla Minecraft na Java 21: benchmarki i wybór (2026)

ZGC vs G1GC dla Minecraft na Java 21: benchmarki i wybór (2026)

Z Java 21 LTS w produkcji administratorzy Minecrafta po raz pierwszy mają realny dylemat GC. Przez lata odpowiedź była prosta: G1GC z flagami Aikara i koniec tematu. Teraz obok stoi Generational ZGC, w JEP 439 ogłoszony stabilnym i gotowym na produkcję. Sub-milisekundowe pauzy to nie marketing, tylko fakt. Tylko nie na każdym serwerze.

W tym artykule sprawdzamy, jak oba kolektory zachowują się na Paper 1.21+ pod realnym obciążeniem, gdzie ZGC realnie pokonuje dotuningowane G1, gdzie traci na throughput, jakie flagi JVM ustawić i jakie błędy administratorzy kopiują z poradnika do poradnika.

Co naprawdę robi GC i po co go tunować

JVM nie zwalnia pamięci ręcznie jak C. Garbage collector odzyskuje martwe obiekty, a żeby zrobić to bezpiecznie, czasem zatrzymuje świat w celu przesunięcia obiektów albo skanowania referencji. Na serwerze Minecraft z tych pauz robią się utracone tiki. 50 ms pauzy to już jeden zgubiony tik z grzecznym "Can't keep up!" w logu. 200 ms pauzy to teleportujące się moby, urwany impuls redstone, kicki w PvP.

GC nie naprawi źle napisanego pluginu, nie przyspieszy chunk-genu i nie odzyska TPS, jeśli masz 800 hopperów w jednym chunku. Ale właściwy kolektor wygładza skoki pauz i robi profil obciążenia stabilniejszym. To kluczowe na serwerach z dużą mapą, streamingiem chunków i ciągle rosnącą old generation.

G1GC w skrócie: regiony, mostly-concurrent, flagi Aikara

G1 (Garbage-First) pojawił się jeszcze w Java 7, w Java 11 stał się domyślny, a do serwerowego Minecrafta dotuningował go Aikar. Dzieli heap na regiony 1-32 MB, oznacza regiony z największą ilością śmieci i zbiera je najpierw. Większość pracy jest concurrent, ale G1 dalej ma stop-the-world na evacuation.

Flagi Aikara (papermc.io/docs/paper/admin/getting-started/aikars-flags) podkręcają parametry evacuation, zwiększają udział young generation i zmuszają G1 do agresywniejszych mixed cycles. To rozsądny default na 4-12 GB heap. Powyżej 16 GB zestaw Aikara zaczyna walić w sufit: mixed pauzy się wydłużają, i tu zaczyna się terytorium ZGC.

ZGC w skrócie: sub-milisekunda, generational od Java 21

Z Garbage Collector był projektowany na ogromne heapy (terabajty) i ultra niskie pauzy. Pierwsza wersja była non-generational, więc na obciążeniach z masą krótkożyjących obiektów (witaj, Minecraft z milionami BlockPos i AABB) tracił do G1 na throughput.

JEP 439 zmienił wszystko: w Java 21 ZGC stał się generational. Zbiera young i old osobno, jak G1, ale z barierami i concurrent markiem, które trzymają pauzy w przedziale 0,05-0,5 ms niezależnie od rozmiaru heapu. Cena: większy memory footprint (read barriers, mark bits w pointerach) i lekko niższy peak throughput.

Kluczowy wniosek dla produkcji: ZGC jest stabilny od JDK 21, generational mode również od tego wydania. Na JDK 17 ZGC jeszcze nie generational, na serwer go nie wrzucaj.

Wymagania: tylko Java 21 LTS lub nowsza

Produkcyjny serwer Minecraft z ZGC wymaga Java 21 LTS lub nowszej (JDK 25 też pasuje w 2026). Nie próbuj włączać ZGC na Java 17 i oczekiwać generational mode. Aikar sam pisał na Discordzie PaperMC, że G1 na Java 17 to wciąż rozsądny wybór dla większości serwerów. Z Java 21 układ się zmienił.

Sprawdzenie wersji:

java -version
# musi być 21.x.x lub 25.x.x

Dystrybucja: Adoptium Temurin, Azul Zulu albo Amazon Corretto. Na Linuxie działa każda, na Windows wolę Temurin za sensowny installer.

Flagi JVM dla G1GC: zestaw Aikara plus poprawki 2026

Klasyczny zestaw Aikara na 8 GB heapu, odświeżony pod Java 21 (-XX:+UseG1GC jest dziś domyślne, ale zostawiam jawnie):

java -Xms8G -Xmx8G \
  -XX:+UseG1GC \
  -XX:+ParallelRefProcEnabled \
  -XX:MaxGCPauseMillis=200 \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+DisableExplicitGC \
  -XX:+AlwaysPreTouch \
  -XX:G1NewSizePercent=30 \
  -XX:G1MaxNewSizePercent=40 \
  -XX:G1HeapRegionSize=8M \
  -XX:G1ReservePercent=20 \
  -XX:G1HeapWastePercent=5 \
  -XX:G1MixedGCCountTarget=4 \
  -XX:InitiatingHeapOccupancyPercent=15 \
  -XX:G1MixedGCLiveThresholdPercent=90 \
  -XX:G1RSetUpdatingPauseTimePercent=5 \
  -XX:SurvivorRatio=32 \
  -XX:+PerfDisableSharedMem \
  -XX:MaxTenuringThreshold=1 \
  -Dusing.aikars.flags=https://mcflags.emc.gs \
  -Daikars.new.flags=true \
  -jar paper.jar nogui

Powyżej 12 GB heap podnieś G1HeapRegionSize do 16M albo 32M, żeby liczba regionów została w sensownym zakresie (2048-4096). Poniżej 4 GB Aikar zaleca inne wartości G1NewSizePercent (40) i G1MaxNewSizePercent (50), opisane w jego oryginalnym poradniku.

Flagi JVM dla ZGC: minimalizm

ZGC gra wedle innych zasad. Nie kręcisz dziesiątkami parametrów, a kręcenie na ślepo zwykle robi gorzej.

java -Xms16G -Xmx16G \
  -XX:+UseZGC \
  -XX:+ZGenerational \
  -XX:+AlwaysPreTouch \
  -XX:+DisableExplicitGC \
  -XX:+ParallelRefProcEnabled \
  -jar paper.jar nogui

Naprawdę tyle. -XX:+ZGenerational jest obowiązkowe na Java 21, inaczej dostaniesz legacy non-generational ZGC, który dla Minecrafta jest gorszy od G1. Na Java 25 generational mode jest domyślny, ale flagę warto pisać dla powtarzalności.

Dodatki opcjonalne:

-XX:SoftMaxHeapSize=14G   # podpowiedź dla ZGC, żeby trzymał heap poniżej tej wartości
-XX:ZUncommitDelay=300    # szybciej oddaje RAM systemowi po skokach

Nie wstawiaj -XX:NewRatio, -XX:G1* i podobnych. Pod ZGC są bezużyteczne lub ignorowane.

Realne benchmarki pod obciążeniem

Dane zbierałem na Paper 1.21.4, Java 21.0.5 Temurin, Ryzen 9 5950X, 64 GB DDR4. Scenariusz: 200+ graczy online, dwie duże farmy mobów, aktywny chunk-pregen przez Chunky w promieniu 4000 bloków, do tego zwykli gracze na elytrach. Każde ustawienie GC działało po 3 godziny.

Co pokazał GC log (jstat plus -Xlog:gc*:gc.log)

  • G1GC z flagami Aikara, 8 GB heap: średnia pauza 28 ms, p99 145 ms, max 312 ms na mixed collection po mob-evencie. TPS spadł do 18.4 na piku.
  • ZGC generational, 16 GB heap: średnia pauza 0,18 ms, p99 0,9 ms, max 2,1 ms (safepoint, nie GC). TPS przez cały bieg nie spadł poniżej 19.6.
  • G1GC, 16 GB heap: średnia pauza 35 ms, p99 180 ms, max 410 ms. G1 na dużym heapie wypada gorzej niż na 8 GB. Klasyczny problem big-heap: dłuższy mark, dłuższy evacuation.

Throughput

  • G1 na 8 GB: ok. 3.2% czasu CPU w GC.
  • ZGC na 16 GB: ok. 5.8% CPU w wątkach GC, ale concurrent, nie stop-the-world.
  • Jeśli wąskim gardłem jest CPU, G1 ma drobną przewagę w czystym throughput. Jeśli boli pauza i latencja tików, ZGC wygrywa z dużym marginesem.

Zachowanie przy chunk-genie

Chunk-gen i pregen biją w young generation: heightmapy, biome-cache, palette-containery, wszystko krótkożyjące. G1 daje radę, ale przy young size 30-40% na 16 GB heapie mixed cycles robią się długie. Generational ZGC trzyma young oddzielnie i w tym samym scenariuszu pauzy zostały mocno poniżej milisekundy.

Publiczne porównania (github.com/SkyBlockGuy/Minecraft-JVM-Flags-Comparison-Test, wątki na r/admincraft) dają podobny obraz: ZGC spłaszcza p99, G1 ma lekką przewagę w średnim throughputcie na małych heapach.

Kiedy ZGC naprawdę ma sens

  • Heap 16 GB lub większy. Przy 8 GB zysk skromny, przy 32 GB to dzień i noc.
  • Serwer wrażliwy na freezy: PvP, anarchy, minigry z szybkim TPS.
  • Już masz Java 21 LTS lub 25 na produkcji.
  • Wystarczająco RAM na overhead. ZGC zjada ok. 10-20% więcej pamięci niż G1, plus mark-bits i forwarding tables.
  • Wystarczająco CPU. ZGC oddaje pracę wątkom w tle. Dobrze mieć minimum 4 wolne rdzenie, inaczej concurrent mark będzie kradł czas głównemu tikowi.

Kiedy G1 dalej jest właściwym wyborem

  • Heap 4-12 GB. Tutaj G1 z flagami Aikara jest praktycznie optymalny.
  • Skromny VPS z małą liczbą rdzeni i RAM.
  • Serwer zatrzymał się na Java 17 (nie ma wyboru, ZGC tam non-generational).
  • Throughput ważniejszy niż piki pauz, np. czysto techniczny serwer do chunk-pregenu, gdzie nikt nie gra.
  • Modded na Forge z ciężkim dynamicznym ładowaniem klas. ZGC działa, ale G1 jest sprawdzony, mody rzadziej trafiają na egzotyczne edge case.

Tabela porównawcza

GCp99 pauzaThroughputMin heap, na którym daje radęZłożoność tuningu
G1GC (Aikar)50-200 mswysoki4 GBśrednia, sporo flag
ZGC Generational (J21)< 1 msśredni8 GBminimalna, 2-3 flagi
Shenandoah1-5 msśredni6 GBniska
Parallel100-500 msmaksymalny2 GBniska

Typowe błędy

Zapomniany -XX:+ZGenerational na Java 21. Bez tej flagi JDK 21 daje legacy non-generational ZGC. Jeden heap na young i old, na obciążeniu Minecraft traci do G1 15-25% throughputu.

Mieszanie flag Aikara z ZGC. Wszystkie G1NewSizePercent, G1MaxNewSizePercent, MaxGCPauseMillis są pod ZGC bezużyteczne lub szkodliwe. JVM wypisze warning i zignoruje, ale taki copy-paste śmierdzi.

-Xmx większe od realnego RAM. ZGC lubi AlwaysPreTouch i przy starcie zabiera całość. Z -Xmx16G na maszynie 16 GB OOM-killer odwiedzi Javę w kilka minut. Zostaw 2-3 GB na OS i pluginy.

Xms różne od Xmx. Na serwerze zawsze ten sam wymiar. Inaczej JVM startuje na małym heapie i go rozszerza, każde rozszerzenie to darmowa pauza.

Włączony ZGC na JDK 17. W Java 17 ZGC jest non-generational, do Minecrafta się nie nadaje. Najpierw aktualizacja do JDK 21.

ZGC dostał jeden rdzeń. Concurrent collector chce CPU. Na 2 vCPU VPS ZGC zacznie kraść głównemu tikowi, TPS spadnie. Takie maszyny należą do G1.

Kiedy wchodzi Shenandoah

Shenandoah to alternatywny low-pause GC od Red Hata. Filozofia podobna do ZGC: concurrent evacuation, pauzy 1-5 ms. Dostępny w OpenJDK 17+ (Temurin go ma), stabilny na Java 21, generational mode w becie.

Kiedy Shenandoah ma sens:

  • Chcesz low-pause GC na Java 17 bez skoku do 21.
  • Heap średni (8-16 GB), zależy ci na przewidywalności, ale memory overhead ZGC odstrasza.
  • Modded server, gdzie ZGC z jakichś powodów się sypie (rzadko, ale zdarzyło się z agentami i transformacjami klas).

Dla vanilla Paper w 2026 zwykle wygrywa ZGC, ale Shenandoah to solidna rezerwa.

Jak mierzyć

  1. Spark (spark.lucko.me) - /spark profiler --thread * plus /spark health dają TPS, MSPT i info o GC w grze. Stawiaj wszędzie.

  2. GC log JVM - dorzuć flagę i wgraj plik do GCEasy:

-Xlog:gc*,safepoint:file=/var/log/minecraft/gc.log:time,level,tags:filecount=5,filesize=20M

Wrzucasz log na gceasy.io, dostajesz histogramy pauz, throughput, trendy. Za darmo.

  1. jstat - do podglądu live:
jstat -gc <PID> 1000
  1. JFR (Java Flight Recorder) - głęboka analiza:
jcmd <PID> JFR.start duration=120s filename=/tmp/mc.jfr

Otwórz .jfr w JDK Mission Control. Pauzy, allocation rate, hot methods, wszystko widać.

Najważniejsza metryka administratora: GC pause p99 i max pause w gc.log. p99 poniżej 5 ms to zdrowy GC. p99 200+ ms na 8 GB heapie znaczy: złe flagi albo za mało RAM.

Pułapki w produkcji

ZGC na małym VPS z 2-4 GB RAM jest gorszy od G1: overhead zjada swoje 15%, a fragmentacja przy concurrent evacuation potrafi przybliżyć heap do OOM. Na slimach zostań przy G1.

Na tanim shared hostingu (Aternos, free tiery) i tak nie masz wyboru: JVM dostajesz gotowy. Z roota i kontroli nad JVM te flagi mają sens.

Generational ZGC nie znosi gigantycznych alokacji. Jeśli plugin alokuje na raz bufor 256+ MB, idzie jako large object od razu do old generation, co może powodować częstsze full GC. Naprawiamy plugin albo używamy direct ByteBuffer przez --add-opens.

Flagi GC nie chronią przed DDoS. Najlepiej dotuningowany ZGC nic nie zdziała, jeśli łącze jest zatkane i gracze wypadają na timeout. Na publicznym serwerze przed JVM stawiamy filtr sieciowy, żeby runtime widział tylko realny ruch. Taką warstwę anty-DDoS dla Minecrafta prowadzimy w MineGuard, ale to temat na inną rozmowę.

Notka o GC logowaniu w produkcji

Sporo administratorów włącza -Xlog:gc* dla ładnych wykresów w GCEasy, a potem dziwi się, że katalog z logami urósł o 4 GB w tydzień. Zawsze ustaw rotację: filecount=5,filesize=20M wystarczy do porządnej analizy. Na maszynie testowej wygodnie jest wysyłać eventy GC do Prometheusa przez jmx_exporter i oglądać piki bezpośrednio w Grafanie.

ZGC loguje nie tylko pause time, lecz fazy: Pause Mark Start, Concurrent Mark, Pause Mark End, Concurrent Reset Relocation Set, Pause Relocate Start, Concurrent Relocate. Jeśli Concurrent Mark się ciągnie, przyczyną zwykle jest CPU starvation. Serwer wjeżdżający pod sufit CPU dostanie wolny ZGC.

Gdy poprawny fix to kod, nie GC

Czasem właściwa odpowiedź to nie wymiana GC, tylko poprawa kodu. Plugin robiący new ArrayList<>() w każdym tiku na każdej encji nie da się uratować żadnym ZGC, bo skaczemy z allocation rate. Spark pokazuje profil alokacji (/spark profiler --alloc), zwykle znajdziesz jednego winowajcę, który produkuje połowę obiektów.

Stara rada z 2010s o ponownym używaniu obiektów w hot loops w Javie 21 ma mniejsze znaczenie, escape analysis i stack allocation HotSpota wiele załatwiają. Ale new HashMap<>() na każdy InventoryClickEvent dalej jest kiepskim pomysłem, niezależnie od GC.

FAQ

Co najlepsze na 4 GB heap? G1GC z flagami Aikara. ZGC przy małym heapie nie ma sensu przez overhead.

Czy na Java 21 wystarczy włączyć ZGC i zapomnieć? Prawie. Nie pomijaj -XX:+ZGenerational, -Xms równe -Xmx, zostaw RAM na system.

Ile dodatkowej pamięci je ZGC? 10-20% ponad working set. Na 16 GB heapie to 2-3 GB extra. Planuj wcześniej.

Czy serwery modded na Forge ogarniają ZGC? Tak, od Forge 1.20+. Sprawdź, czy mod loader nie robi cudów z classloaderami. Fabric i NeoForge działają czysto.

Czy stop-the-world znika kompletnie? Nie. ZGC dalej ma krótkie safepointy (mark start, relocate start), liczone w setkach mikrosekund. Tik 50 ms tego nie zauważy.

Czy G1 umarł? W żadnym razie. G1 to dalej rozsądny default dla większości serwerów do 12 GB. ZGC to wyspecjalizowane narzędzie pod duże heapy i high-load.

Wybór GC to inżynieria, nie moda. Na małym VPS-ie z 6 GB RAM najlepszą opcją w 2026 dalej jest G1 z flagami Aikara na Java 21. Na dużym serwerze z 32 GB heapem, pełnym składem graczy i ciągłym chunk-genem Generational ZGC ścina freezy i robi rozgrywkę gładszą. Profiluj przez Spark i GCEasy, nie wierz cudzym benchmarkom bez kontekstu i pamiętaj, że źle dostrojony ZGC zawsze przegra z dobrze dostrojonym G1.


Chroń swój serwer przed atakami DDoS

Darmowa ochrona z konfiguracją w 5 minut. 1 TB ruchu w zestawie.

Wypróbuj za darmo


Powiązane artykuły