En introduktion til ZGC: En skalerbar og eksperimentel JVM Garbage Collector med lav latens

1. Introduktion

I dag er det ikke ualmindeligt, at applikationer tjener tusinder eller endda millioner af brugere samtidigt. Sådanne applikationer har brug for enorme mængder hukommelse. At styre al den hukommelse kan dog let påvirke applikationsydelsen.

For at løse dette problem introducerede Java 11 Z Garbage Collector (ZGC) som en eksperimentel affaldssamler (GC) implementering.

I denne vejledning ser vi hvordan ZGC formår at holde lave pausetider på selv bunker med flere terabyte.

2. Hovedkoncepter

For at forstå, hvordan ZGC fungerer, er vi nødt til at forstå de grundlæggende begreber og terminologi bag hukommelsesstyring og affaldssamlere.

2.1. Hukommelsesstyring

Fysisk hukommelse er det RAM, som vores hardware leverer.

Operativsystemet (OS) tildeler virtuel hukommelsesplads til hver applikation.

Selvfølgelig, vi gemmer virtuel hukommelse i fysisk hukommelse, og operativsystemet er ansvarlig for at opretholde kortlægningen mellem de to. Denne kortlægning involverer normalt hardwareacceleration.

2.2. Multi-kortlægning

Multi-mapping betyder, at der er specifikke adresser i den virtuelle hukommelse, der peger på den samme adresse i den fysiske hukommelse. Da applikationer får adgang til data gennem virtuel hukommelse, ved de intet om denne mekanisme (og det behøver de ikke).

Effektivt kortlægger vi flere områder af den virtuelle hukommelse til det samme område i den fysiske hukommelse:

Ved første øjekast er dets brugssager ikke indlysende, men vi vil se senere, at ZGC har brug for det for at gøre sin magi. Det giver også en vis sikkerhed, fordi det adskiller applikationernes hukommelsesrum.

2.3. Flytning

Da vi bruger dynamisk hukommelsesallokering, bliver hukommelsen til en gennemsnitlig applikation fragmenteret over tid. Det er fordi, når vi frigør et objekt midt i hukommelsen, forbliver der et hul af ledig plads. Over tid akkumuleres disse huller, og vores hukommelse vil ligne et skakbræt lavet af skiftende områder med frit og brugt plads.

Naturligvis kunne vi prøve at udfylde disse huller med nye objekter. For at gøre dette skal vi scanne hukommelsen for ledig plads, der er stor nok til at holde vores objekt. Det er en dyr operation at gøre dette, især hvis vi skal gøre det hver gang vi vil allokere hukommelse. Desuden vil hukommelsen stadig være fragmenteret, da vi sandsynligvis ikke kan finde et ledigt rum, der har den nøjagtige størrelse, vi har brug for. Derfor vil der være huller mellem objekterne. Selvfølgelig er disse huller mindre. Vi kan også prøve at minimere disse huller, men det bruger endnu mere processorkraft.

Den anden strategi er at ofte flytte objekter fra fragmenterede hukommelsesområder til at frigøre områder i et mere kompakt format. For at være mere effektiv deler vi hukommelsespladsen i blokke. Vi flytter alle objekter i en blok eller ingen af ​​dem. På denne måde vil hukommelsestildeling være hurtigere, da vi ved, at der er helt tomme blokke i hukommelsen.

2.4. Dagrenovation

Når vi opretter en Java-applikation, behøver vi ikke frigøre den hukommelse, vi tildelte, fordi affaldssamlere gør det for os. Sammenfattende GC ser på hvilke objekter, vi kan nå fra vores applikation gennem en kæde af referencer, og frigør dem, vi ikke kan nå.

En GC skal spore objekternes tilstand i bunkerummet for at udføre sit arbejde. For eksempel er en mulig tilstand tilgængelig. Det betyder, at applikationen har en henvisning til objektet. Denne reference kan være midlertidig. Det eneste der betyder noget, at applikationen kan få adgang til disse objekter gennem referencer. Et andet eksempel kan færdiggøres: objekter, som vi ikke kan få adgang til. Det er de objekter, vi betragter som affald.

For at opnå det har affaldssamlere flere faser.

2.5. GC-faseegenskaber

GC-faser kan have forskellige egenskaber:

  • -en parallel fase kan køre på flere GC-tråde
  • -en seriel fase kører på en enkelt tråd
  • -en stop-verdenen fase kan ikke køre samtidigt med applikationskode
  • -en samtidig fase kan køre i baggrunden, mens vores applikation gør sit arbejde
  • en inkrementel fase kan afsluttes, før alt dets arbejde er afsluttet og fortsætte senere

Bemærk, at alle ovennævnte teknikker har deres styrker og svagheder. Lad os for eksempel sige, at vi har en fase, der kan køre samtidigt med vores applikation. En seriel implementering af denne fase kræver 1% af den samlede CPU-ydelse og kører i 1000 ms. I modsætning hertil bruger en parallel implementering 30% af CPU'en og fuldfører sit arbejde på 50 ms.

I dette eksempel er parallel løsning bruger mere CPU generelt, fordi det kan være mere komplekst og skal synkronisere trådene. For CPU-tunge applikationer (for eksempel batchjob) er det et problem, da vi har mindre computerkraft til at udføre nyttigt arbejde.

Selvfølgelig har dette eksempel sammensatte numre. Det er dog klart, at alle applikationer har deres egenskaber, så de har forskellige GC-krav.

For mere detaljerede beskrivelser, se vores artikel om Java-hukommelsesstyring.

3. ZGC-koncepter

ZGC agter at give stop-the-world faser så korte som muligt. Det opnår det på en sådan måde, at varigheden af ​​disse pausetider ikke øges med dyngestørrelsen. Disse egenskaber gør ZGC til en god pasform til serverapplikationer, hvor store dynger er almindelige, og hurtige responstider for applikationer er et krav.

Ud over de afprøvede GC-teknikker introducerer ZGC nye koncepter, som vi vil dække i de følgende afsnit.

Men nu skal vi se på det overordnede billede af, hvordan ZGC fungerer.

3.1. Store billede

ZGC har en fase kaldet markering, hvor vi finder de tilgængelige objekter. En GC kan gemme objekttilstandsinformation på flere måder. For eksempel kunne vi oprette en Kort, hvor tasterne er hukommelsesadresser, og værdien er objektets tilstand på den adresse. Det er enkelt, men har brug for ekstra hukommelse for at gemme disse oplysninger. Det kan også være udfordrende at vedligeholde et sådant kort.

ZGC bruger en anden tilgang: den gemmer referencetilstanden som referencens bits. Det kaldes referencefarvning. Men på denne måde har vi en ny udfordring. Indstilling af bits i en reference til lagring af metadata om et objekt betyder, at flere referencer kan pege på det samme objekt, da tilstandsbitene ikke indeholder nogen information om placeringen af ​​objektet. Multimapping til undsætning!

Vi vil også mindske fragmenteringen af ​​hukommelsen. ZGC bruger flytning for at opnå dette. Men med en stor bunke er flytning en langsom proces. Da ZGC ikke ønsker lange pausetider, flytter det meste af det parallelt med applikationen. Men dette introducerer et nyt problem.

Lad os sige, at vi har en henvisning til et objekt. ZGC flytter det, og der opstår en kontekstskift, hvor applikationstråden kører og forsøger at få adgang til dette objekt gennem sin gamle adresse. ZGC bruger belastningsbarrierer til at løse dette. En belastningsbarriere er et stykke kode, der kører, når en tråd indlæser en reference fra bunken - for eksempel når vi får adgang til et ikke-primitivt felt af et objekt.

I ZGC kontrollerer belastningsbarrierer referencens metadatabit. Afhængigt af disse bits, ZGC udfører muligvis en vis behandling af referencen, før vi får den. Derfor kan det producere en helt anden reference. Vi kalder dette remapping.

3.2. Mærkning

ZGC opdeler markering i tre faser.

Den første fase er en stop-the-world-fase. I denne fase ser vi efter rodreferencer og markerer dem. Rodhenvisninger er udgangspunktet for at nå objekter i bunkenf.eks. lokale variabler eller statiske felter. Da antallet af rodreferencer normalt er lille, er denne fase kort.

Den næste fase er samtidig. I denne fase vi krydser objektgrafen, startende fra rodreferencer. Vi markerer hvert objekt, vi når. Når en belastningsbarriere registrerer en umærket reference, markerer den også den.

Den sidste fase er også en stop-the-world-fase til at håndtere nogle kantsager, som svage referencer.

På dette tidspunkt ved vi, hvilke objekter vi kan nå.

ZGC bruger markeret0 og markeret1 metadata bits til markering.

3.3. Reference farve

En reference repræsenterer placeringen af ​​en byte i den virtuelle hukommelse. Vi behøver dog ikke nødvendigvis at bruge alle bits i en reference til at gøre det - nogle bits kan repræsentere egenskaberne for referencen. Det er det, vi kalder referencefarvning.

Med 32 bits kan vi adressere 4 gigabyte. Da det i dag er udbredt for en computer at have mere hukommelse end dette, kan vi naturligvis ikke bruge nogen af ​​disse 32 bits til farvning. Derfor bruger ZGC 64-bit referencer. Det betyder ZGC er kun tilgængelig på 64-bit platforme:

ZGC-referencer bruger 42 bits til at repræsentere selve adressen. Som et resultat kan ZGC-referencer adressere 4 terabyte hukommelsesplads.

Derudover har vi 4 bits til at gemme referencetilstande:

  • kan færdiggøres bit - objektet kan kun nås via en finalizer
  • omlægge bit - referencen er opdateret og peger på objektets aktuelle placering (se flytning)
  • markeret0 og markeret1 bits - disse bruges til at markere tilgængelige objekter

Vi kaldte også disse bits metadata bits. I ZGC er netop en af ​​disse metadatabit 1.

3.4. Flytning

I ZGC består flytning af følgende faser:

  1. En samtidige fase, der ser efter blokke, vi vil flytte og placere dem i flytningssættet.
  2. En stop-the-world-fase flytter alle rodreferencer i flytningsættet og opdaterer deres referencer.
  3. En samtidig fase flytter alle resterende objekter i flytningsættet og gemmer kortlægningen mellem den gamle og den nye adresse i videresendelsestabellen.
  4. Omskrivningen af ​​de resterende referencer sker i den næste markeringsfase. På denne måde behøver vi ikke krydse genstandstræet to gange. Alternativt kan belastningsbarrierer også gøre det.

3.5. Omlægning og belastningsbarrierer

Bemærk, at vi i omplaceringsfasen ikke omskrev de fleste referencer til de flyttede adresser. Derfor, ved hjælp af disse referencer, ville vi ikke få adgang til de objekter, vi ønskede at. Endnu værre, vi kunne få adgang til affald.

ZGC bruger belastningsbarrierer til at løse dette problem. Belastningsbarrierer retter referencerne, der peger på flyttede objekter med en teknik kaldet remapping.

Når applikationen indlæser en reference, udløser den belastningsbarrieren, som derefter følger følgende trin for at returnere den korrekte reference:

  1. Kontrollerer, om omlægge bit er indstillet til 1. Hvis ja, betyder det, at referencen er opdateret, så vi kan sikkert returnere den.
  2. Derefter kontrollerer vi, om det refererede objekt var i flytningsættet eller ej. Hvis det ikke var tilfældet, betyder det, at vi ikke ønskede at flytte det. For at undgå denne kontrol næste gang vi indlæser denne reference, indstiller vi omlægge bit til 1, og returner den opdaterede reference.
  3. Nu ved vi, at det objekt, vi vil have adgang til, var målet for flytning. Det eneste spørgsmål er, om flytningen skete eller ej? Hvis objektet er blevet flyttet, springer vi videre til næste trin. Ellers flytter vi det nu og opretter en post i videresendelsestabellen, der gemmer den nye adresse for hvert genflyttet objekt. Herefter fortsætter vi med det næste trin.
  4. Nu ved vi, at objektet blev flyttet. Enten af ​​ZGC, os i det forrige trin, eller belastningsbarrieren under et tidligere hit af dette objekt. Vi opdaterer denne henvisning til objektets nye placering (enten med adressen fra det forrige trin eller ved at slå den op i videresendelsestabellen), indstil omlægge bit, og returner referencen.

Og det er det, med trinene ovenfor sørgede vi for, at hver gang vi prøver at få adgang til et objekt, får vi den seneste henvisning til det. Da hver gang vi indlæser en reference, udløser det belastningsbarrieren. Derfor nedsætter det applikationsydelsen. Især første gang vi får adgang til et flyttet objekt. Men dette er en pris, vi skal betale, hvis vi ønsker korte pausetider. Og da disse trin er relativt hurtige, påvirker det ikke applikationsydelsen væsentligt.

4. Hvordan aktiveres ZGC?

Vi kan aktivere ZGC med følgende kommandolinjemuligheder, når vi kører vores applikation:

-XX: + UnlockExperimentalVMOptions -XX: + UseZGC

Bemærk, at da ZGC er en eksperimentel GC, vil det tage lidt tid at blive officielt understøttet.

5. Konklusion

I denne artikel så vi, at ZGC har til hensigt at understøtte store dyngestørrelser med lave applikationspausetider.

For at nå dette mål bruger den teknikker, herunder farvede 64-bit referencer, belastningsbarrierer, flytning og omlægning.


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