En introduksjon til ZGC: A Scalable and Experimental Low-Latency JVM Garbage Collector

1. Introduksjon

I dag er det ikke uvanlig at applikasjoner betjener tusenvis eller til og med millioner av brukere samtidig. Slike applikasjoner trenger enorme mengder minne. Å administrere alt minnet kan imidlertid lett påvirke applikasjonsytelsen.

For å løse dette problemet introduserte Java 11 Z Garbage Collector (ZGC) som en eksperimentell søppeloppsamler (GC) implementering.

I denne opplæringen får vi se hvordan ZGC klarer å holde lave pausetider på til og med flere terabyte hauger.

2. Hovedkonsepter

For å forstå hvordan ZGC fungerer, må vi forstå de grunnleggende konseptene og terminologien bak minnestyring og søppeloppsamlere.

2.1. Minnehåndtering

Fysisk minne er RAM som maskinvaren vår gir.

Operativsystemet (OS) tildeler virtuell minneplass for hvert program.

Selvfølgelig, vi lagrer virtuelt minne i fysisk minne, og operativsystemet er ansvarlig for å opprettholde kartleggingen mellom de to. Denne kartleggingen innebærer vanligvis maskinvareakselerasjon.

2.2. Multikartlegging

Multikartlegging betyr at det er spesifikke adresser i det virtuelle minnet, som peker på samme adresse i det fysiske minnet. Siden applikasjoner får tilgang til data gjennom virtuelt minne, vet de ingenting om denne mekanismen (og det trenger de ikke).

Effektivt kartlegger vi flere områder av det virtuelle minnet til samme område i det fysiske minnet:

Ved første øyekast er brukssakene ikke åpenbare, men vi får se senere at ZGC trenger det for å gjøre sin magi. Det gir også litt sikkerhet fordi det skiller minneplassene til applikasjonene.

2.3. Flytting

Siden vi bruker dynamisk minnetildeling, blir minnet til et gjennomsnittlig program fragmentert over tid. Det er fordi når vi frigjør et objekt midt i minnet, forblir et gap med ledig plass der. Over tid akkumuleres disse hullene, og minnet vårt vil se ut som et sjakkbrett laget av vekslende områder med ledig og brukt plass.

Selvfølgelig kan vi prøve å fylle disse hullene med nye objekter. For å gjøre dette, bør vi skanne minnet for ledig plass som er stor nok til å holde objektet vårt. Å gjøre dette er en kostbar operasjon, spesielt hvis vi må gjøre det hver gang vi vil tildele minne. Dessuten vil minnet fortsatt være fragmentert, siden vi sannsynligvis ikke vil kunne finne en ledig plass som har den nøyaktige størrelsen vi trenger. Derfor vil det være hull mellom gjenstandene. Selvfølgelig er disse hullene mindre. Vi kan også prøve å minimere disse hullene, men det bruker enda mer prosessorkraft.

Den andre strategien er å ofte flytte objekter fra fragmenterte minneområder til ledige områder i et mer kompakt format. For å være mer effektiv deler vi minneområdet i blokker. Vi flytter alle objekter i en blokk eller ingen av dem. På denne måten vil minnetildeling bli raskere siden vi vet at det er hele tomme blokker i minnet.

2.4. Søppelsamling

Når vi oppretter et Java-program, trenger vi ikke å frigjøre minnet vi tildelte, fordi søppeloppsamlere gjør det for oss. Oppsummert, GC ser på hvilke objekter vi kan nå fra applikasjonen vår gjennom en kjede av referanser og frigjør de vi ikke kan nå.

En GC må spore tilstanden til objektene i dyngrommet for å gjøre sitt arbeid. For eksempel er en mulig tilstand tilgjengelig. Det betyr at applikasjonen har en referanse til objektet. Denne referansen kan være midlertidig. Det eneste som betyr noe at applikasjonen kan få tilgang til disse objektene gjennom referanser. Et annet eksempel kan fullføres: objekter som vi ikke får tilgang til. Dette er gjenstandene vi betrakter som søppel.

For å oppnå det har søppeloppsamlere flere faser.

2.5. GC-fasegenskaper

GC-faser kan ha forskjellige egenskaper:

  • en parallell fase kan kjøres på flere GC-tråder
  • en seriell fase går på en enkelt tråd
  • en stopp-verdenen fase kan ikke kjøres samtidig med applikasjonskode
  • en samtidig fase kan kjøre i bakgrunnen, mens applikasjonen vår gjør sitt
  • en trinnvis fasen kan avsluttes før du fullfører alt arbeidet og fortsetter det senere

Merk at alle ovennevnte teknikker har sine styrker og svakheter. La oss for eksempel si at vi har en fase som kan kjøre samtidig med applikasjonen vår. En seriell implementering av denne fasen krever 1% av den totale CPU-ytelsen og kjører i 1000 ms. I motsetning til dette bruker en parallell implementering 30% av CPU og fullfører arbeidet på 50 ms.

I dette eksemplet er parallell løsning bruker mer CPU generelt, fordi det kan være mer komplekst og må synkronisere trådene. For CPU-tunge applikasjoner (for eksempel batchjobber), er det et problem siden vi har mindre datakraft til å gjøre nyttig arbeid.

Selvfølgelig har dette eksemplet sminketall. Det er imidlertid klart at alle applikasjoner har sine egenskaper, så de har forskjellige GC-krav.

For mer detaljerte beskrivelser, besøk vår artikkel om Java-minneadministrasjon.

3. ZGC-konsepter

ZGC har til hensikt å tilby stopp-faser så korte som mulig. Det oppnår det på en slik måte at varigheten av disse pausetidene ikke øker med hopestørrelsen. Disse egenskapene gjør at ZGC passer godt for serverapplikasjoner, der store dynger er vanlige, og raske responstider for applikasjoner er et krav.

På toppen av de velprøvde GC-teknikkene introduserer ZGC nye konsepter, som vi vil dekke i de følgende avsnittene.

Men for nå, la oss ta en titt på det samlede bildet av hvordan ZGC fungerer.

3.1. Stort bilde

ZGC har en fase som kalles markering, hvor vi finner gjenstandene som kan nås. En GC kan lagre informasjon om objekttilstand på flere måter. For eksempel kan vi lage en Kart, hvor nøklene er minneadresser, og verdien er tilstanden til objektet på den adressen. Det er enkelt, men trenger ekstra minne for å lagre denne informasjonen. Å vedlikeholde et slikt kart kan også være utfordrende.

ZGC bruker en annen tilnærming: den lagrer referansetilstanden som referansens biter. Det kalles referansefarging. Men på denne måten har vi en ny utfordring. Å sette biter av en referanse for å lagre metadata om et objekt betyr at flere referanser kan peke på det samme objektet siden tilstandsbitene ikke inneholder noen informasjon om plasseringen av objektet. Multimapping til unnsetning!

Vi ønsker også å redusere minnefragmentering. ZGC bruker flytting for å oppnå dette. Men med en stor haug er flytting en langsom prosess. Siden ZGC ikke vil ha lange pausetider, gjør det mesteparten av flyttingen parallelt med applikasjonen. Men dette introduserer et nytt problem.

La oss si at vi har en referanse til et objekt. ZGC flytter det, og det oppstår en kontekstbryter, der applikasjonstråden kjører og prøver å få tilgang til dette objektet gjennom sin gamle adresse. ZGC bruker lastbarrierer for å løse dette. En lastbarriere er et stykke kode som går når en tråd laster inn en referanse fra dyngen - for eksempel når vi får tilgang til et ikke-primitivt felt av et objekt.

I ZGC sjekker lastbarrierer metadatabitene til referansen. Avhengig av disse bitene, ZGC kan utføre noen behandlinger på referansen før vi får den. Derfor kan det gi en helt annen referanse. Vi kaller dette remapping.

3.2. Merking

ZGC deler markering i tre faser.

Den første fasen er en stopp-verden-fase. I denne fasen ser vi etter rotreferanser og markerer dem. Rothenvisninger er utgangspunktet for å nå objekter i haugen, for eksempel lokale variabler eller statiske felt. Siden antallet rotreferanser vanligvis er lite, er denne fasen kort.

Neste fase er samtidig. I denne fasen, vi krysser objektgrafen, fra rotreferansene. Vi markerer hvert objekt vi når. Når en lastbarriere oppdager en umerket referanse, markerer den også den.

Den siste fasen er også en stopp-verden-fase for å håndtere noen kantsaker, som svake referanser.

På dette tidspunktet vet vi hvilke objekter vi kan nå.

ZGC bruker merket0 og merket1 metadatabiter for merking.

3.3. Referanse fargelegging

En referanse representerer posisjonen til en byte i det virtuelle minnet. Vi trenger imidlertid ikke nødvendigvis å bruke alle biter av en referanse for å gjøre det - noen biter kan representere egenskapene til referansen. Det er det vi kaller referansefarging.

Med 32 biter kan vi adressere 4 gigabyte. Siden det i dag er utbredt for en datamaskin å ha mer minne enn dette, kan vi åpenbart ikke bruke noen av disse 32 bitene til farging. Derfor bruker ZGC 64-biters referanser. Det betyr ZGC er bare tilgjengelig på 64-biters plattformer:

ZGC-referanser bruker 42 biter for å representere selve adressen. Som et resultat kan ZGC-referanser adressere 4 terabyte minne.

På toppen av det har vi fire biter for å lagre referansetilstander:

  • kan fullføres bit - objektet er bare tilgjengelig via en finalizer
  • kartlegge om bit - referansen er oppdatert og peker på den gjeldende plasseringen av objektet (se flytting)
  • merket0 og merket1 biter - disse brukes til å merke gjenstander som kan nås

Vi kalte også disse bitene metadatabiter. I ZGC er akkurat en av disse metadatabitene 1.

3.4. Flytting

I ZGC består flytting av følgende faser:

  1. En samtidig fase, som ser etter blokker, vil vi flytte og plassere dem i flyttesettet.
  2. En stopp-verden-fase flytter alle rotreferanser i flyttesettet og oppdaterer referansene.
  3. En samtidig fase flytter alle gjenværende objekter i flyttesettet og lagrer kartleggingen mellom den gamle og den nye adressen i videresendingstabellen.
  4. Omskrivingen av de gjenværende referansene skjer i neste merkingsfase. På denne måten trenger vi ikke å krysse objektet tre ganger. Alternativt kan lastbarrierer også gjøre det.

3.5. Kartlegging og lastbarrierer

Merk at vi i omplasseringsfasen ikke skrev om de fleste referansene til de flyttede adressene. Derfor, ved å bruke disse referansene, ville vi ikke få tilgang til objektene vi ønsket. Enda verre, vi kunne få tilgang til søppel.

ZGC bruker lastbarrierer for å løse dette problemet. Lastbarrierer fikser referansene som peker på flyttede objekter med en teknikk som kalles remapping.

Når applikasjonen laster inn en referanse, utløser den belastningsbarrieren, som følger følgende trinn for å returnere riktig referanse:

  1. Sjekker om kartlegge om bit er satt til 1. Hvis ja, betyr det at referansen er oppdatert, så kan vi trygt returnere den.
  2. Deretter sjekker vi om det refererte objektet var i omplasseringssettet eller ikke. Hvis det ikke var det, betyr det at vi ikke ønsket å flytte det. For å unngå denne kontrollen neste gang vi laster inn denne referansen, setter vi kartlegge om bit til 1 og returner den oppdaterte referansen.
  3. Nå vet vi at objektet vi ønsker å få tilgang til var målet for flytting. Det eneste spørsmålet er om flyttingen skjedde eller ikke? Hvis objektet har blitt flyttet, hopper vi til neste trinn. Ellers flytter vi det nå og oppretter en oppføring i videresendingstabellen, som lagrer den nye adressen for hvert flyttede objekt. Etter dette fortsetter vi med neste trinn.
  4. Nå vet vi at objektet ble flyttet. Enten av ZGC, oss i forrige trinn, eller lastbarrieren under et tidligere treff av dette objektet. Vi oppdaterer denne referansen til den nye plasseringen av objektet (enten med adressen fra forrige trinn eller ved å slå den opp i videresendingstabellen), angir kartlegge om bit, og returner referansen.

Og det er det, med trinnene ovenfor sørget vi for at hver gang vi prøver å få tilgang til et objekt, får vi den siste referansen til det. Siden hver gang vi laster inn en referanse, utløser det lastbarrieren. Derfor reduserer applikasjonsytelsen. Spesielt første gang vi får tilgang til et flyttet objekt. Men dette er en pris vi må betale hvis vi ønsker korte pausetider. Og siden disse trinnene er relativt raske, påvirker det ikke applikasjonsytelsen betydelig.

4. Hvordan aktivere ZGC?

Vi kan aktivere ZGC med følgende kommandolinjealternativer når du kjører applikasjonen vår:

-XX: + UnlockExperimentalVMOptions -XX: + UseZGC

Merk at siden ZGC er en eksperimentell GC, vil det ta litt tid å bli offisielt støttet.

5. Konklusjon

I denne artikkelen så vi at ZGC har til hensikt å støtte store dyngstørrelser med lave pausetider for applikasjoner.

For å nå dette målet bruker den teknikker, inkludert fargede 64-biters referanser, lastbarrierer, flytting og kartlegging.


$config[zx-auto] not found$config[zx-overlay] not found