Duboko razumevanje JVM sakupljača smeća: CMS, G1, ZGC
Za Java zajednicu, sakupljanje smeća je tema koja se ne može zaobići; posao optimizacije na poslu se takođe često vrti oko sakupljača smeća. Pred različitim poslovnim scenarijima, često su potrebni različiti sakupljači smeća da bi se obezbedile GC performanse, zato oni koji žele na intervju u velike kompanije ili imaju visoke ambicije mogu dublje zaći u sakupljače smeća.
Trenutno se JVM sakupljači smeća uglavnom dele u dve kategorije: generacioni sakupljači i sakupljači po oblastima. Predstavnik generacionih sakupljača je CMS, a predstavnici sakupljača po oblastima su G1 i ZGC; hajde da pogledamo ove dve velike kategorije sakupljača smeća.

Generacioni sakupljači
CMS
S ciljem postizanja najkraćeg vremena pauze pri recikliranju, koristi algoritam "označavanje-brisanje" i vrši sakupljanje smeća u 4 velika koraka, od kojih početno označavanje i ponovno označavanje izazivaju STW. Uveden u JDK 1.5, u JDK 9 označen kao zastareo, a u JDK 14 uklonjen; detalje možete videti u JEP 363.
CMS (Concurrent Mark Sweep) sakupljač smeća prvi je sakupljač koji je obratio pažnju na GC vreme pauze (vreme STW-a). Prethodni sakupljači smeća su ili bili serijski način sakupljanja smeća, ili su se bavili samo propusnim opsegom sistema.
Razlog zbog kojeg CMS sakupljač smeća uspeva da kontroliše GC vreme pauze u suštini proizlazi iz poboljšanja algoritma analize dostižnosti, odnosno trobojnog algoritma označavanja. Pre pojave CMS-a, bilo da je u pitanju Serious sakupljač smeća, ParNew sakupljač smeća ili Parallel Scavenge sakupljač smeća, svi su pri sakupljanju smeća morali da zaustave svet (Stop the World) i nisu mogli da ostvare istovremeno izvršavanje niti za sakupljanje smeća i korisničkih niti.
Algoritam označavanje-brisanje, Stop the World, algoritam analize dostižnosti i sl. obrađivali smo u prethodnom poglavlju; oni koji su zaboravili mogu ponovo pregledati.
CMS sakupljač smeća je pomoću trobojnog algoritma označavanja ostvario istovremeno izvršavanje niti za sakupljanje smeća i korisničkih niti, čime je drastično smanjio vreme odziva sistema i poboljšao iskustvo aplikacija sa jakom interakcijom. Njegov proces rada deli se na 4 koraka, uključujući:
- Početno označavanje
- Konkurentno označavanje
- Ponovno označavanje
- Konkurentno čišćenje
Početno označavanje se odnosi na pronalaženje svih objekata koje referenciraju GCRoots; ova faza zahteva "Stop the World". Ovaj korak samo označava objekte do kojih GC Roots može direktno doći i ne zahteva skeniranje celog grafa referenci, pa je veoma brz.
Konkurentno označavanje se odnosi na skeniranje celog lanca referenci objekata označenih u fazi "početnog označavanja"; ova faza ne zahteva "Stop the World". Skeniranje celog lanca referenci zahteva vrlo mnogo vremena, pa istovremenim izvršavanjem niti za sakupljanje smeća i korisničkih niti može se smanjiti vreme sakupljanja smeća.
Ovo je i suštinski razlog zbog kojeg CMS uspeva da drastično smanji GC vreme pauze, ali to sa sobom donosi i neke probleme, i to: prilikom konkurentnog označavanja reference se mogu promeniti, pa može doći do propuštenog označavanja (smeće koje je trebalo reciklirati nije reciklirano) i višestrukog označavanja (smeće koje nije trebalo reciklirati jeste reciklirano).
Ponovno označavanje se odnosi na ispravljanje problema koji su se pojavili u fazi "konkurentnog označavanja"; ova faza zahteva "Stop the World". Kao što je rečeno u fazi konkurentnog označavanja, pošto se algoritam sakupljanja smeća i korisničke niti izvršavaju istovremeno, iako se time smanjuje vreme odziva, javljaju se problemi propuštenog i višestrukog označavanja. Zato CMS u ovoj fazi mora izvršiti neke provere i rešiti probleme nastale u fazi konkurentnog označavanja.
Konkurentno čišćenje se odnosi čišćenje objekata označenih kao smeće; ova faza ne zahteva "Stop the World". U ovoj fazi nit za sakupljanje smeća i korisničke niti mogu se izvršavati istovremeno, pa ne utiče na vreme odziva korisnika.

Prednosti CMS-a su: konkurentno sakupljanje, niska pauza. Ali mane su takođe očigledne:
① Veoma je osetljiv na CPU resurse, pa u uslovima oskudice CPU resursa performanse CMS-a drastično opadaju.
Podrazumevano, broj niti za sakupljanje smeća koje CMS aktivira iznosi (broj CPU-a + 3)/4; kada je broj CPU-a veliki, udeo aktiviranih niti za sakupljanje smeća je manji. Ali ako je broj CPU-a mali, na primer samo 2 CPU-a, zauzeće niti za sakupljanje smeća dostiže 50%, što drastično smanjuje propusni opseg sistema i neprihvatljivo je.
② CMS koristi algoritam "označavanje-brisanje", koji stvara veliku količinu fragmenata memorije, zbog čega prostor nije neprekidan; kada veliki objekat ne može da pronađe neprekidan memorijski prostor, to će okinuti jedan Full GC, što dovodi do produženja vremena pauze sistema.
③ CMS ne može da obradi plutajuće smeće; kada CMS vrši sakupljanje smeća, aplikacija i dalje neprestano stvara smeće. To smeće će nastati nakon završetka CMS-ovog sakupljanja smeća — to je plutajuće smeće; CMS ne može da obradi to plutajuće smeće, već može samo da ga očisti pri sledećem GC-u.
Sakupljači po oblastima
G1
G1 (Garbage-First Garbage Collector) uveden je u JDK 1.7, a u JDK 9 zamenio je CMS i postao podrazumevani sakupljač smeća. G1 ima pet svojstava: generacioni, inkrementalni, paralelni, označavanje-sređivanje, STW.
① Generacioni: verujem da se još sećate mlade i stare generacije iz našeg prethodnog predavanja — G1 je takođe dizajniran na osnovu te ideje. Heap memoriju deli na više oblasti jednake veličine (Region), pri čemu svaka oblast može biti Eden, Survivor ili Old oblast.

Parametrom -XX:G1HeapRegionSize=n može se podesiti veličina Region-a; može se postaviti na 1M, 2M, 4M, 8M, 16M, 32M (ne veće).
G1 ima poseban Region za dodelu velikih objekata koji se zove Humongous oblast, umesto da se veliki objekti smeštaju direktno u Region stare generacije. U G1, pravilo za utvrđivanje velikih objekata jeste da objekat prelazi 50% veličine jednog Region-a; na primer, ako je svaki Region 2M, čim objekat premaši 1M, biće smešten u Humongous, a ako je veoma velik, može obuhvatiti i više Region-a.
G1 na osnovu situacije sakupljanja smeća u svakoj oblasti odlučuje koja oblast će biti predmet sledećeg sakupljanja smeća, čime se izbegava sakupljanje smeća nad celim heap memorijom i skraćuje vreme sakupljanja.
② Inkrementalni: G1 može izvršavati sakupljanje smeća na inkrementalni način, što znači da ne mora odjednom reciklirati čitav heap prostor, već može postepeno, inkrementalno čistiti. To pomaže u kontroli vremena pauze, naročito pri obradi velikog heap-a.
③ Paralelni: G1 sakupljač smeća može paralelno sakupljati smeće, što znači da može iskoristiti više CPU-a da ubrza sakupljanje smeća. Ova osobina je naročito izražena u sakupljanju smeća mlade generacije (Minor GC), jer sakupljanje mlade generacije obično obuhvata više objekata i ima veću stopu recikliranja.
④ Označavanje-sređivanje: prilikom sakupljanja smeća stare generacije, G1 koristi algoritam označavanje-sređivanje. Ovaj proces se deli u dve faze: označavanje preživelih objekata i sređivanje (kompresija) heap prostora. Sređivanjem G1 uspeva da izbegne fragmentaciju memorije i poboljša iskorišćenost memorije.
Sakupljanje smeća mlade generacije (Minor GC) koristi algoritam kopiranja, jer su objekti u mladoj generaciji obično kratkog veka.

⑤ STW: G1 je takođe zasnovan na algoritmu "označavanje-brisanje", pa prilikom sakupljanja smeća i dalje zahteva "Stop the World". Međutim, G1 je vremenu pauze dodao mehanizam predviđanja — korisnik može navesti željeno vreme pauze.

U G1 postoje tri GC režima: Young GC, Mixed GC i Full GC.

Kada memorijski prostor Eden oblasti ne može da podrži dodelu memorije novom objektu, G1 će okinuti Young GC.
Kada je potrebno dodeliti objekte u Humongous oblast ili kada zauzeće heap memorije premaši vrednost InitiatingHeapOccupancyPercent podešenu parametrom -XX:G1HeapWastePercent, G1 će okinuti jedno concurrent marking, čija je uloga da izračuna koliko prostora u staroj generaciji podleže recikliranju; kada se utvrdi da udeo smeća dostiže proporciju G1HeapWastePercent podešenu u -XX:G1HeapWastePercent, nakon sledećeg Young GC-a biće okinut jedan Mixed GC.
Mixed GC se odnosi na recikliranje Region-a mlade generacije i dela Region-a stare generacije. Mixed GC, kao i Young GC, koristi algoritam kopiranja.
U toku Mixed GC-a, ako se utvrdi da prostor u staroj generaciji i dalje nije dovoljan, a G1HeapWastePercent je podešen suviše nisko, to može izazvati Full GC. -XX:G1HeapWastePercent je podrazumevano 5, što znači da je samo 5% heap-a "bačeno". Ako procenat bačenog heap-a veći od G1HeapWastePercent, pokreće se Full GC.
Na osnovu toga što je Region najmanja jedinica upravljanja i na osnovu GC režima koji se koristi, G1 je izgradio model predviđanja pauze, tzv. Pause Prediction Model. To je i jedna od osobina G1-a koja se najviše hvali.
Možemo pomoću -XX:MaxGCPauseMillis podesiti željeno vreme pauze (podrazumevano 200ms); G1 će na osnovu te vrednosti izračunati razumno vreme recikliranja za Young GC, a zatim na osnovu tog vremena sastaviti plan recikliranja za Young GC.
ZGC
ZGC (The Z Garbage Collector) jeste sakupljač smeća niske latencije predstavljen u JDK 11, namenjen upravljanju memorijom i recikliranju za servise sa velikom memorijom i niskom latencijom. U SPEC jbb 2015 benchmark testu, na heap-u od 128G maksimalno vreme pauze iznosilo je samo 1,68 ms — vreme pauze znatno bolje od G1 i CMS.
Cilj dizajna ZGC-a jeste: pri vremenu pauze ne dužem od 10ms, podrška za TB-nivoe memorijskog kapaciteta i gotovo sve GC funkcije. Odatle potiče i ime ZGC — Z označava Zettabyte, odnosno 1024 EB, odnosno 1024 puta 1TB.
Međutim, moram vam reći da je gorenavedeni pasus izmišljen (😂); zvanični JDK nije jasno dao definiciju Z-a, baš kao što je kod Xiaomi automobila su7 — i 7 je magični broj bez jasne definicije.
Ukratko, ZGC je zaista moćan; njegovi ciljevi su:
- Vreme pauze ne duže od 10ms;
- Vreme pauze se ne povećava sa veličinom heap-a niti sa veličinom aktivnih objekata;
- Podrška za heap od 8MB do 4TB, u budućnosti i 16TB.
Gore, prilikom obrade G1 sakupljača smeća, pomenuto je da i Young GC i Mixed GC koriste algoritam kopiranja. Algoritam kopiranja uglavnom obuhvata sledeće 3 faze:
① Faza označavanja — polazeći od GC Roots, analizira se dostižnost objekata i označavaju aktivni objekti.

② Faza prenosa objekata — aktivni objekti se kopiraju na nove memorijske adrese.
③ Faza relokacije — pošto prenos dovodi do promene adresa objekata, u ovoj fazi sve reference koje ukazuju na stare adrese objekata moraju se prilagoditi na nove adrese objekata.
Faza označavanja je kratka jer se označavaju samo GC Roots. Ali faza prenosa i faza relokacije moraju obraditi sve preživele objekte i dugotrajne su, a faza prenosa je STW, pa je usko grlo performansi G1 uglavnom u fazi prenosa.
Slično kao G1 i CMS, ZGC takođe koristi algoritam kopiranja, ali je značajno optimizovan — kod ZGC-a su faze označavanja, prenosa i relokacije gotovo sve konkurentne, što je ključ za postizanje vremena pauze ispod 10ms.

Kako ZGC to uspeva?
- Bojenje pokazivača (Colored Pointer): tehnika koja se koristi za obeležavanje stanja objekata.
- Barijera pri čitanju (Load Barrier): posebna provera koja se umeće u operacije pristupa objektima tokom rada programa, a služi da osigura ispravnost pristupa objektima.
Ove dve tehnike omogućavaju svim nitima da, pod uslovima konkurentnosti, postignu saglasnost oko boje (stanja) pokazivača, a ne oko adrese objekta. Zato ZGC može konkurentno da kopira objekte, što drastično smanjuje GC vreme pauze.
Bojenje pokazivača
U jednom pokazivaču, pored čuvanja stvarne adrese objekta, dodatni bitovi se koriste za čuvanje metapodataka o tom objektu. Te informacije mogu obuhvatati:
- Da li je objekat premešten (odnosno da li je tokom recikliranja premešten na novu poziciju).
- Status preživljavanja objekta.
- Da li je objekat zaključan ili je u nekom drugom posebnom stanju.
Ugrađivanjem ovih informacija u pokazivač, ZGC je brži u fazama označavanja i prenosa, jer se na osnovu boje na pokazivaču može razlikovati stanje objekta, bez dodatnog pristupa memoriji.
ZGC podržava samo 64-bitne sisteme; on 64-bitni virtuelni adresni prostor deli na više potprostora, kao što je prikazano na slici:

Pri tome, 0-4TB odgovara Java heap-u, 4TB-8TB se naziva M0 adresni prostor, 8TB-12TB se naziva M1 adresni prostor, 12TB-16TB je rezervisano i nekoršćeno, a 16TB-20TB se naziva Remapped prostor.
Prilikom kreiranja objekta, prvo se u heap prostoru zahteva jedna virtuelna adresa, koja se ne mapira na stvarnu fizičku adresu. Istovremeno, ZGC će u M0, M1 i Remapped prostoru za taj objekat zatražiti po jednu virtuelnu adresu, pri čemu sve tri virtuelne adrese mapiraju se na istu fizičku adresu.
Na sledećoj slici je prikazana podela prostora virtuelnih adresa:

Međutim, od ta tri prostora u istom trenutku samo je jedan aktivan. Razlog što ZGC uvodi ove tri virtuelne adrese jeste što ZGC prihvata ideju "zamene prostora za vreme" radi smanjenja GC vremena pauze.
U skladu sa gorenavedenom podelom adresnog prostora, ZGC zapravo koristi samo bitove 0-41 od 64-bitnog adresnog prostora, dok bitovi 42-45 čuvaju metapodatke, a bitovi 47-63 su fiksirani na 0.

Pošto se za čuvanje adrese objekta koriste samo bitovi 0~43, a
Što se tiče informacija o preživljavanju objekta, one se čuvaju u bitovima 42-45, što je potpuno drugačije od tradicionalnog sakupljanja smeća gde se te informacije čuvaju u zaglavlju objekta.
Barijera pri čitanju
Kada program pokuša da pročita jedan objekat, barijera pri čitanju okida sledeće operacije:
- Provera boje pokazivača: barijera pri čitanju prvo proverava informaciju o boji pokazivača koji ukazuje na objekat.
- Obrada premeštenih objekata: ako pokazivač ukazuje da je objekat premešten (na primer, tokom sakupljanja smeća), barijera pri čitanju će osigurati da se vrati nova pozicija objekta.
- Obezbeđenje konzistentnosti: na ovaj način ZGC uspeva da održi konzistentnost pristupa memoriji dok konkurentno pomiče objekte, čime se smanjuje potreba za pauziranjem aplikacije.
Kako se implementira ZGC-ova barijera pri čitanju?
Pogledajmo sledeći pseudokod, koji se odnosi na C++ kod na dnu JVM-a:
// Primer pseudokoda koji prikazuje konceptualnu implementaciju barijere pri čitanju
Object* read_barrier(Object* ref) {
if (is_forwarded(ref)) {
return get_forwarded_address(ref); // preuzimanje nove adrese objekta
}
return ref; // objekat nije pomeran, vraća se originalna referenca
}- read_barrier predstavlja barijeru pri čitanju.
- Ako je objekat premešten (is_forwarded(ref)), metoda vraća novu adresu objekta (get_forwarded_address(ref)).
- Ako objekat nije premešten, metoda vraća originalnu referencu na objekat.
Barijeru pri čitanju mogu okinuti i GC niti i poslovne niti, a okidaće se samo pri pristupu objektima u heap-u; pristup objektima koji se nalaze u GC Roots-u neće je okinuti, što je i razlog zašto pri skeniranju GC Roots-a mora postojati STW.
U nastavku je jednostavljen primer koda koji prikazuje trenutak okidanja barijere pri čitanju.
Object o = obj.FieldA // čitanje reference iz heap-a, potrebno je dodati barijeru
<Load barrier>
Object p = o // nema potrebe za barijerom, jer se ne čita referenca iz heap-a
o.dosomething() // nema potrebe za barijerom, jer se ne čita referenca iz heap-a
int i = obj.FieldB // nema potrebe za barijerom, jer nije u pitanju referenca na objekatRadni proces ZGC-a
ZGC ciklus se sastoji od tri STW pauze i četiri konkurentne faze: označavanje/relociranje (M/R), konkurentna obrada referenci (RP), konkurentna priprema prenosa (EC) i konkurentni prenos (RE).

Stop-The-World faze pauze
Početak označavanja (Mark Start) STW pauza: ovo je početak ZGC-a; vrši se početno označavanje GC Roots-a. Tokom ove kratke pauze ZGC označava sve objekte direktno dostižne iz GC Root-a.
Početak relokacije (Relocation Start) STW pauza: nakon konkurentne faze, ova STW pauza služi za pripremu relokacije objekata. U ovoj fazi ZGC bira memorijske oblasti koje će biti očišćene i uspostavlja potrebne strukture podataka za premeštanje objekata.
Kraj pauze (Pause End) STW pauza: ZGC se završava. U ovoj kratkoj pauzi završava se sav rad na finalnom čišćenju povezanom sa tim GC ciklusom.
Konkurentne faze
Konkurentno označavanje/relokacija (M/R): ova faza obuhvata konkurentno označavanje i konkurentnu relokaciju. U konkurentnom označavanju ZGC obilazi graf objekata i označava sve dostižne objekte. Zatim, u konkurentnoj relokaciji, ZGC ažurira sve reference koje ukazuju na pomerene objekte.
Konkurentna obrada referenci (RP): u ovoj fazi ZGC obrađuje različite tipove referenci (kao što su softve, slabe, fantomske i reference na duhove). Obrada ovih referenci obično zahteva posebno razmatranje, jer su usko povezane sa dostižnošću objekata i njihovim životnim ciklusom.
Konkurentna priprema prenosa (EC): ovo je faza pripreme za prenos objekata. ZGC utvrđuje koje će memorijske oblasti biti očišćene i priprema relevantne strukture podataka.
Konkurentni prenos (RE): u ovoj fazi ZGC premešta preživele objekte sa starih na nove pozicije. Pošto se ovaj proces odvija konkurentno, aplikacija može nastaviti sa radom dok se obavlja veći deo sakupljanja smeća.

Dve ključne tehnike ZGC-a — bojenje pokazivača i barijera pri čitanju — ne primenjuju se samo u fazi konkurentnog prenosa, već i u fazi konkurentnog označavanja: da bi se objekat označio, tradicionalni sakupljač smeća mora izvršiti jedan pristup memoriji i informaciju o preživljavanju objekta smestiti u zaglavlje objekta; u ZGC-u je dovoljno podesiti bitove 42-45 adrese pokazivača, a pošto je u pitanju pristup registru, brzina je veća nego pri pristupu memoriji.

Rezime
U ovom članku smo uglavnom predstavili tri sakupljača smeća — CMS, G1 i ZGC; to su sve sakupljači po oblastima, nastali s ciljem smanjenja GC pauze, ali svaki ima svoje prednosti i mane, pa u skladu sa poslovnim scenarijima možemo odabrati odgovarajući sakupljač smeća.
Reference:
- Shuge razgovara o programiranju: CMS sakupljač smeća
- Junge razgovara o tehnologiji: G1 sakupljač smeća
- Stručnjak za tehnologiju Meituan: Neke ključne tehnike G1 GC-a
- Geek Time: Zašto se G1 naziva kraljem GC-a
- Dewu tehnologija: Analiza ključnih tehnika ZGC-a
- Meituan tehnologija: Istraživanje i praksa ZGC-a
- CoderW: ZGC sakupljač smeća
