Shenandoah, představení low latency garbage collectoru pro JVM

Shenandoah je garbage collector, jehož hlavní cíl je minimalizace stop-the-world (STW) pauz, tj. snížení latence běžícího programu. Jde o open source vyvíjený převážně v Red Hatu a je součástí OpenJDK od verze 11.0.9 od většiny dodavatelů (vyjma Oraclu). Red Hat pak backportuje Shenandoah i do OpenJDK 8.

Obecně by se práce garbage collectoru dala rozdělit do dvou hlavních fází. První fází je tzv. mark – identifikace nepoužívaných objektů (resp. naopak, identifikace živých objektů, zbylá paměť je považována za odpad). Druhou je pak úklid nepoužívaných objektů (v případě Shenadoah je použitý compaction algoritmus). Shenadoah obě tyto fáze provádí s využitím více threadů, cílem je většinu práce vykonávat souběžně s běžícím programem a minimalizovat tak STW pauzy.

Základní nastavení a popis fází

Shenandoah můžeme ve spouštěné JVM nastavit parametrem -XX:+UseShenandoahGC.

Cyklus se skládá z několika fází, kde STW pauzy jsou vyznačené červeně. Pokud vše běží jak má, typická pauza se pohybuje v rozmezí 1-10 ms. Počet threadů vyhrazených pro práci během pauz je možné nastavit JVM parametrem -XX:ParallelGCThreads. Délka pauz není daná velikostí alokované paměti, ale velikostí tzv. „root set“ – vrcholů prohledávaného grafu. Příkladem můžou být lokální proměnné, vstupní parametry právě spouštěných metod, aktivní thready, static fields, nebo JNI reference.

Po pauzách následují tzv. souběžné (concurrent) fáze, kdy GC běží společně s programem, jenž vytváří nové a mění/ruší reference mezi objekty. Shenandoah se se situací musí vypořádat a zároveň vykonat svou práci, ať už  jde o detekci aktivních objektů ve fázi mark, nebo jejich přesun (evacuation) do jiných regionů a čistku prázdných regionů. Počet vláken určených pro souběžnou fázi je daný parametrem -XX:ConcGCThreads, hodnota musí být rovná, nebo nižší než ParallelGCThreads. Souběžné fáze trvají řádově stovky milisekund.

https://wiki.openjdk.java.net/display/shenandoah/Main

Paměťová náročnost

Shenandoah je designován i pro práci s velmi velkými haldami (stovky GB). Velikost paměti určené pro JVM je dané parametry -Xms a -Xmx (minimum zabrané při startu a maximum). Pokud je minimální latence programu prioritou, doporučené je nastavit oba parametry stejně velké a doplnit je parametrem -XX:+AlwaysPreTouch. Toto nastavení zabere veškerou přidělenou paměť při startu pro okamžité použití virtuálním strojem. Odstraní se tak zpomalení, která by vznikaly, pokud by se paměť pro JVM přidělovala postupně za běhu.

Kdy zahájit cyklus je rozhodnutím poměrně složité heuristiky. Běžně je však startován až když aplikace alokuje 90% veškeré dostupné paměti, předchází se tak zbytečným startům a je zajištěné, že kolektor „má co dělat“. Všechna tato nastavení a chování výrazně zvyšují paměťový otisk (memory footprint) aplikace, nároky na paměť jsou značné.

Nejlepších výsledků kolektor dosahuje, pokud je velikost živých (dostupných) objektů okolo 30% přidělené paměti. Pozorováním jsem zjistil, že při utilizaci nad 80% začínají problémy a Shenandoah se přepíná do tzv. degenerovaného módu, kde STW pauzy dosahují stovek milisekund až sekund. Na to však nemá vliv jen utilizace, ale i tempo alokace nové paměti (memory pressure), Shenandoah v podstatě závodí s programem v alokaci a uvolňování objektů. Pokud začne kolektor prohrávat, nejprve zpomalí alokaci nové paměti (tzv. pacing) a pokud ani to nepomůže,  přepne se do zmiňovaného degenerovaného módu.

Pokud pomineme, že některé kolektory jsou zkrátka lépe napsané, volba a tuning garbage collectoru jsou vždy výměnou mezi latencí a propustností (throughput) programu. Dává to smysl, paralelní a konkurenční vlákna vyhrazená pro běh kolektoru nemohou při cyklu obsluhovat samotný program a režie poměrně komplikovaného cyklu také něco stojí. V praxi jde zhruba o desetiprocentní snížení propustnosti, záleží samozřejmě na porovnávaném kolektoru.

Novinky

Na závěr zmíním ještě 2 novinky z vývoje Shenandoah. Tou první je tzv. concurrent stack processing, kdy je root set zpracovávaný souběžně. Délka STW pauzy pak nezávisí primárně na velikosti root setu a naměřené pauzy se běžně pohybovaly pod 1 ms. Tato funkce je bohužel dostupná pouze pro JDK 17 a na nižší verze nelze z technických důvodů backportovat.

Druhou „novinkou“ je pak zapojení Amazonu do vývoje. Nezmínil jsem, že aktuálně je Shenandoah tzv. non-generational, tzn. paměť není rozdělená do generací, jak je tomu typicky u jiných GC. Amazon, resp. Corretto JVM team momentálně pracuje na zavedení tzv. young a old generace. Ty budou obsluhovány různými cykly, mladá generace mnohem častěji. Cílem je zejména snížit nároky na utilizaci paměti (zmiňovaných optimálních 30%) při vysokých allocation rates. Prototyp by mohl být dostupný pro JDK 17.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *