Programmering

JVM-ytelsesoptimalisering, del 3: Søppeloppsamling

Java-plattformens søppeloppsamlingsmekanisme øker utviklerens produktivitet betydelig, men en dårlig implementert søppeloppsamler kan overforbruke applikasjonsressurser. I denne tredje artikkelen i JVM-ytelsesoptimalisering Eva Andreasson tilbyr Java-nybegynnere en oversikt over Java-plattformens minnemodell og GC-mekanisme. Hun forklarer deretter hvorfor fragmentering (og ikke GC) er den viktigste "gotcha!" av Java-applikasjonsytelse, og hvorfor generasjons søppelinnsamling og komprimering for tiden er de ledende (men ikke mest innovative) tilnærmingene til å håndtere haugfragmentering i Java-applikasjoner.

Søppelsamling (GC) er prosessen som tar sikte på å frigjøre okkupert minne som ikke lenger er referert til av noe tilgjengelig Java-objekt, og som er en viktig del av Java virtual machine (JVMs) dynamiske minnehåndteringssystem. I en typisk søppeloppsamlingssyklus oppbevares alle gjenstander som fremdeles er referert til, og som nå kan nås. Plassen okkupert av tidligere refererte objekter frigjøres og gjenvinnes for å muliggjøre ny objektallokering.

For å forstå søppeloppsamling og de forskjellige GC-tilnærminger og algoritmer, må du først vite noen ting om Java-plattformens minnemodell.

JVM-ytelsesoptimalisering: Les serien

  • Del 1: Oversikt
  • Del 2: Kompilatorer
  • Del 3: Søppelinnsamling
  • Del 4: Samtidig komprimering av GC
  • Del 5: Skalerbarhet

Søppelinnsamling og Java-plattformminnemodellen

Når du angir oppstartsalternativet -Xmx på kommandolinjen til Java-applikasjonen (for eksempel: java -Xmx: 2g MyApp) minne tilordnes en Java-prosess. Dette minnet er referert til som Java haug (eller bare haug). Dette er den dedikerte minneadresseplassen der alle objekter opprettet av Java-programmet ditt (eller noen ganger JVM) vil bli tildelt. Ettersom Java-programmet ditt fortsetter å kjøre og tildele nye objekter, fylles Java-dyngen (som betyr adresseområdet).

Til slutt vil Java-haugen være full, noe som betyr at en tildelingstråd ikke klarer å finne en stor nok sammenhengende del ledig minne for objektet den vil tildele. På det tidspunktet bestemmer JVM at en søppeloppsamling må skje, og den varsler søppeloppsamleren. En søppelsamling kan også utløses når et Java-program ringer System.gc (). Ved hjelp av System.gc () garanterer ikke søppeloppsamling. Før en eventuell søppeloppsamling kan starte, vil en GC-mekanisme først avgjøre om det er trygt å starte det. Det er trygt å starte en søppelsamling når alle applikasjonens aktive tråder er på et trygt punkt for å tillate det, f.eks. forklarte ganske enkelt at det ville være ille å starte søppelinnsamling midt i en pågående objektallokering, eller midt i å utføre en sekvens av optimaliserte CPU-instruksjoner (se min forrige artikkel om kompilatorer), da du kan miste kontekst og derved ødelegge slutten resultater.

En søppeloppsamler burde aldri gjenvinne et aktivt referert objekt; å gjøre det ville ødelegge spesifikasjonen for den virtuelle Java-maskinen. En søppeloppsamler er heller ikke pålagt å samle døde gjenstander umiddelbart. Døde gjenstander blir til slutt samlet under påfølgende søppeloppsamlingssykluser. Selv om det er mange måter å implementere søppeloppsamling på, gjelder disse to antagelsene for alle varianter. Den virkelige utfordringen med søppeloppsamling er å identifisere alt som er live (fremdeles referert til) og gjenvinne ethvert ikke-referert minne, men gjør det uten å påvirke kjørende applikasjoner mer enn nødvendig. En søppeloppsamler har altså to mandater:

  1. Å raskt frigjøre ikke-referert minne for å tilfredsstille en applikasjons allokeringshastighet slik at det ikke går tom for minne.
  2. Å gjenvinne minne mens du minimalt påvirker ytelsen (f.eks. Ventetid og gjennomstrømning) til et program som kjører.

To typer søppelinnsamling

I den første artikkelen i denne serien berørte jeg de to viktigste tilnærmingene til søppeloppsamling, som er referansetelling og sporing av samlere. Denne gangen vil jeg gå nærmere ned på hver tilnærming og deretter introdusere noen av algoritmene som brukes til å implementere sporingssamlere i produksjonsmiljøer.

Les JVM-ytelsesoptimaliseringsserien

  • JVM-ytelsesoptimalisering, del 1: oversikt
  • JVM ytelsesoptimalisering, del 2: kompilatorer

Samlere for referansetelling

Samlere for referansetelling holde oversikt over hvor mange referanser som peker mot hvert Java-objekt. Når tellingen for et objekt blir null, kan minnet gjenvinnes umiddelbart. Denne umiddelbare tilgangen til gjenvunnet minne er den største fordelen med referansetellingstilnærmingen til søppeloppsamling. Det er veldig lite overhead når det gjelder å holde på ikke-referert minne. Å holde alle referansetallene oppdatert kan imidlertid være ganske kostbart.

Hovedproblemet med referansetellingssamlere er å holde referansetellene nøyaktige. En annen kjent utfordring er kompleksiteten knyttet til håndtering av sirkulære strukturer. Hvis to objekter refererer til hverandre og ingen levende objekter refererer til dem, vil deres minne aldri bli frigitt. Begge objektene forblir for alltid med en ikke-null-telling. Å gjenvinne minne assosiert med sirkulære strukturer krever større analyser, noe som medfører kostnadskostnader for algoritmen, og dermed for applikasjonen.

Spore samlere

Spore samlere er basert på antagelsen om at alle levende objekter kan bli funnet ved iterativt å spore alle referanser og påfølgende referanser fra et innledende sett med kjent å være levende objekter. Det første settet med levende objekter (kalt rotobjekter eller bare røtter for kort) er lokalisert ved å analysere registre, globale felt og stabelrammer for øyeblikket når en søppeloppsamling utløses. Etter at et innledende live-sett er identifisert, følger sporingssamleren referanser fra disse objektene og køer dem opp for å bli markert som live og deretter få referansene sporet. Merker alle funnet refererte objekter bo betyr at det kjente livesettet øker over tid. Denne prosessen fortsetter til alle refererte (og dermed alle levende) gjenstander blir funnet og merket. Når sporingssamleren har funnet alle levende gjenstander, vil den gjenvinne gjenværende minne.

Sporsamlere skiller seg fra referansetellingssamlere ved at de kan håndtere sirkulære strukturer. Fangsten med de fleste sporingssamlere er merkingsfasen, som innebærer en ventetid før du kan gjenvinne ikke-referert minne.

Sporingsamlere brukes oftest til minnehåndtering på dynamiske språk; de er uten tvil de vanligste for Java-språket og har blitt bevist kommersielt i produksjonsmiljøer i mange år. Jeg vil fokusere på å spore samlere resten av denne artikkelen, og starte med noen av algoritmene som implementerer denne tilnærmingen til søppeloppsamling.

Spore samleralgoritmer

Kopiering og mark-and-sweep søppelinnsamling er ikke ny, men de er fremdeles de to vanligste algoritmene som implementerer sporing av søppelinnsamling i dag.

Kopiering av samlere

Tradisjonelle kopiersamlere bruker en fra-rom og en to-space - det vil si to separat definerte adresserom på dyngen. På tidspunktet for søppeloppsamlingen kopieres de levende objektene i området definert som fra-rom til neste tilgjengelige plass i området definert som til-rom. Når alle levende gjenstander i fra-rommet flyttes ut, kan hele fra-rommet gjenvinnes. Når tildelingen begynner igjen, starter den fra den første ledige plasseringen i to-space.

I eldre implementeringer av denne algoritmen plasseres fra-rom-til-rom-bryteren, noe som betyr at når to-space er full, blir søppeloppsamling utløst igjen og to-space blir fra-space, som vist i figur 1.

Mer moderne implementeringer av kopieringsalgoritmen gjør det mulig å tildele vilkårlige adresserom i haugen til plass og fra plass. I disse tilfellene trenger de ikke nødvendigvis å bytte plassering med hverandre; heller blir hver en annen adresse plass i dyngen.

En fordel med kopiering av samlere er at objekter fordeles tett sammen i rommet, og eliminerer fragmentering fullstendig. Fragmentering er et vanlig problem som andre søppeloppsamlingsalgoritmer sliter med; noe jeg vil diskutere senere i denne artikkelen.

Ulemper ved kopiering av samlere

Kopieringssamlere er vanligvis stopp-verden-samlere, noe som betyr at ingen applikasjonsarbeid kan utføres så lenge søppeloppsamlingen er i syklus. I en verdensimplementering, jo større område du trenger å kopiere, desto større innvirkning vil det ha på applikasjonsytelsen. Dette er en ulempe for applikasjoner som er følsomme for responstid. Med en kopieringssamler må du også vurdere det verste fallet når alt er live i fra-rommet. Du må alltid la nok takhøyde være til at levende gjenstander kan flyttes, noe som betyr at rommet må være stort nok til å være vert for alt i fra-rommet. Kopieringsalgoritmen er litt minneeffektiv på grunn av denne begrensningen.

Merk og fei samlere

De fleste kommersielle JVM-er distribuert i produksjonsmiljøer for bedrifter kjører mark-and-sweep (eller marking) samlere, som ikke har den ytelsespåvirkningen som kopieringssamlere har. Noen av de mest kjente merkesamlerne er CMS, G1, GenPar og DeterministicGC (se Ressurser).

EN mark-and-sweep samler sporer referanser og merker hvert funnet objekt med en "live" bit. Vanligvis tilsvarer en settbit en adresse eller i noen tilfeller et sett med adresser på dyngen. Live-biten kan for eksempel lagres som en bit i objektoverskriften, en bitvektor eller et bitkart.

Etter at alt er merket live, vil feiefasen sparke inn. Hvis en samler har en feiefase, inneholder den i utgangspunktet en eller annen mekanisme for å krysse dyngen igjen (ikke bare live settet, men hele dyngelengden) for å finne alle de ikke-merkede biter av påfølgende minneadresseplasser. Umerket minne er gratis og gjenvinnes. Samleren kobler deretter sammen disse umerkede bitene til organiserte gratis lister. Det kan være forskjellige gratis lister i en søppeloppsamler - vanligvis organisert etter klumpestørrelser. Noen JVM-er (som JRockit Real Time) implementerer samlere med heuristikk som dynamisk viser størrelseslister basert på applikasjonsprofileringsdata og objektstørrelsesstatistikk.

Når feifasen er fullført, vil tildelingen begynne på nytt. Nye tildelingsområder tildeles fra gratislistene, og minnebiter kan matches med objektstørrelser, gjennomsnitt av objektstørrelse per tråd-ID eller applikasjonsinnstilte TLAB-størrelser. Å tilpasse ledig plass nærmere størrelsen på det applikasjonen din prøver å tildele, optimaliserer minne og kan bidra til å redusere fragmentering.

Mer om TLAB-størrelser

Partisjonering av TLAB og TLA (Thread Local Allocation Buffer eller Thread Local Area) blir diskutert i JVM-ytelsesoptimalisering, del 1.

Ulemper med mark-and-sweep samlere

Merkfasen er avhengig av mengden live data på bunken din, mens feiefasen er avhengig av bunnsstørrelsen. Siden du må vente til begge merke og feie fasene er komplette for å gjenvinne minne, forårsaker denne algoritmen pausetidsutfordringer for større dynger og større live datasett.

En måte du kan hjelpe tungt minnekrevende applikasjoner på, er å bruke GC-tuningalternativer som imøtekommer ulike applikasjonsscenarier og behov. Innstilling kan i mange tilfeller i det minste bidra til å utsette en av disse fasene fra å bli en risiko for applikasjonen din eller servicenivåavtaler (SLAer). (En SLA spesifiserer at applikasjonen vil oppfylle bestemte responstider for applikasjonen - dvs. latens.) Innstilling for hver lastendring og applikasjonsendring er imidlertid en repeterende oppgave, da innstillingen bare er gyldig for en bestemt arbeidsmengde og tildelingshastighet.

Implementeringer av mark-and-sweep

Det er minst to kommersielt tilgjengelige og velprøvde tilnærminger for implementering av mark-and-sweep-samling. Den ene er den parallelle tilnærmingen og den andre er den samtidige (eller for det meste samtidige) tilnærmingen.

Parallelle samlere

Parallell samling betyr at ressurser som er tildelt prosessen brukes parallelt til søppeloppsamling. De fleste kommersielt implementerte parallelle samlere er monolitiske stop-the-world samlere - alle applikasjonstråder stoppes til hele søppeloppsamlingssyklusen er fullført. Å stoppe alle tråder gjør at alle ressurser kan brukes effektivt parallelt for å fullføre søppeloppsamlingen gjennom merke- og feiefasen. Dette fører til et veldig høyt effektivitetsnivå, som vanligvis resulterer i høye poengsummer på gjennomstrømningsverdier som SPECjbb. Hvis gjennomstrømning er viktig for søknaden din, er parallell tilnærming et utmerket valg.

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