Programmering

JVM-ydeevneoptimering, del 3: Affaldssamling

Java-platformens skraldopsamlingsmekanisme øger udviklerens produktivitet i høj grad, men en dårligt implementeret affaldssamler kan overforbruge applikationsressourcer. I denne tredje artikel i JVM-optimering Eva Andreasson tilbyder Java-begyndere et overblik over Java-platformens hukommelsesmodel og GC-mekanisme. Derefter forklarer hun, hvorfor fragmentering (og ikke GC) er det største "gotcha!" af Java-applikationsydelse, og hvorfor generation af skraldsamling og komprimering i øjeblikket er de førende (dog ikke mest innovative) tilgange til styring af bunkefragmentering i Java-applikationer.

Dagrenovation (GC) er den proces, der sigter mod at frigøre besat hukommelse, der ikke længere refereres til af noget Java-objekt, der kan nås, og som er en væsentlig del af Java Virtual Machines (JVM's) dynamiske hukommelsesstyringssystem. I en typisk affaldsindsamlingscyklus opbevares alle objekter, der stadig refereres til og dermed nås. Rummet, der besættes af tidligere refererede objekter, frigøres og genvindes for at muliggøre ny objektallokering.

For at forstå affaldsindsamling og de forskellige GC-tilgange og algoritmer skal du først vide et par ting om Java-platformens hukommelsesmodel.

JVM-ydeevneoptimering: Læs serien

  • Del 1: Oversigt
  • Del 2: Kompilatorer
  • Del 3: Affaldssamling
  • Del 4: Samtidig komprimering af GC
  • Del 5: Skalerbarhed

Affaldssamling og Java-platformens hukommelsesmodel

Når du angiver startmuligheden -Xmx på kommandolinjen i din Java-applikation (for eksempel: java -Xmx: 2g MyApp) hukommelse tildeles en Java-proces. Denne hukommelse kaldes Java bunke (eller bare bunke). Dette er det dedikerede hukommelsesadresseområde, hvor alle objekter, der er oprettet af dit Java-program (eller nogle gange JVM), tildeles. Da dit Java-program fortsætter med at køre og tildele nye objekter, fyldes Java-bunken (hvilket betyder det adresseområde).

Til sidst vil Java-bunken være fuld, hvilket betyder, at en tildelingstråd ikke er i stand til at finde en stor nok sammenhængende del af ledig hukommelse til det objekt, den vil tildele. På det tidspunkt bestemmer JVM, at en affaldsindsamling skal ske, og den underretter affaldssamleren. En affaldssamling kan også udløses, når et Java-program ringer System.gc (). Ved brug af System.gc () garanterer ikke en affaldsindsamling. Inden enhver affaldsindsamling kan starte, vil en GC-mekanisme først afgøre, om det er sikkert at starte det. Det er sikkert at starte en affaldssamling, når alle applikationens aktive tråde er på et sikkert sted for at give mulighed for det, f.eks. simpelthen forklaret, at det ville være dårligt at starte affaldsindsamling midt i en igangværende objektallokering eller midt i udførelsen af ​​en række optimerede CPU-instruktioner (se min tidligere artikel om kompilatorer), da du måske mister kontekst og derved ødelægger slutningen resultater.

En affaldssamler skal aldrig genvinde et aktivt refereret objekt at gøre det ville bryde specifikationen for den virtuelle Java-maskine. En affaldssamler er heller ikke forpligtet til straks at indsamle døde genstande. Døde genstande opsamles til sidst under efterfølgende skraldopsamlingscyklusser. Der er mange måder at implementere affaldsindsamling på, men disse to antagelser gælder for alle sorter. Den virkelige udfordring ved skraldindsamling er at identificere alt, hvad der er live (der stadig henvises til) og genvinde enhver hukommelse, der ikke er henvist til, men gør det uden at påvirke kørende applikationer mere end nødvendigt. En affaldssamler har således to mandater:

  1. For hurtigt at frigøre ikke-henvist hukommelse for at tilfredsstille en applikations allokeringshastighed, så den ikke løber tør for hukommelse.
  2. At genvinde hukommelse, mens du minimalt påvirker ydeevnen (f.eks. Latenstid og kapacitet) for et kørende program.

To slags affaldsindsamling

I den første artikel i denne serie berørte jeg de to hovedtilgange til affaldsindsamling, som er referencetælling og sporing af samlere. Denne gang vil jeg gå nærmere ned på hver tilgang og derefter introducere nogle af de algoritmer, der bruges til at implementere sporingssamlere i produktionsmiljøer.

Læs JVM-ydeevneoptimeringsserien

  • JVM-ydeevneoptimering, del 1: oversigt
  • JVM-ydeevneoptimering, del 2: Compilers

Referencetællingssamlere

Referencetællingssamlere holde styr på, hvor mange referencer der peger på hvert Java-objekt. Når tællingen for et objekt bliver nul, kan hukommelsen straks genvindes. Denne øjeblikkelige adgang til genvundet hukommelse er den største fordel ved referencetællingsmetoden til affaldsindsamling. Der er meget lidt overhead, når det kommer til at holde fast i ikke-refereret hukommelse. At holde alle referencetællinger opdateret kan dog være ret dyrt.

Det største problem med referencetællingssamlere er at holde referencetællingerne nøjagtige. En anden velkendt udfordring er kompleksiteten forbundet med håndtering af cirkulære strukturer. Hvis to objekter refererer til hinanden, og ingen levende objekter henviser til dem, frigives deres hukommelse aldrig. Begge objekter forbliver for evigt med en ikke-nul-optælling. Gendannelse af hukommelse forbundet med cirkulære strukturer kræver større analyser, hvilket medfører dyre omkostninger for algoritmen og dermed applikationen.

Sporing af samlere

Sporing af samlere er baseret på antagelsen om, at alle levende objekter kan findes ved iterativt at spore alle referencer og efterfølgende referencer fra et indledende sæt af kendt for at være levende objekter. Det indledende sæt levende objekter (kaldet rodgenstande eller bare rødder for kort) findes ved at analysere registre, globale felter og stabelrammer i det øjeblik, hvor en affaldssamling udløses. Efter at et indledende live-sæt er blevet identificeret, følger sporingssamleren referencer fra disse objekter og sætter dem i kø for at blive markeret som live og efterfølgende spore deres referencer. Markering af alle fundne refererede objekter Direkte betyder, at det kendte live-sæt øges over tid. Denne proces fortsætter, indtil alle henviste (og dermed alle levende) objekter findes og markeres. Når sporingssamleren har fundet alle levende objekter, vil den genvinde den resterende hukommelse.

Sporingssamlere adskiller sig fra referencetællingssamlere, fordi de kan håndtere cirkulære strukturer. Fangsten med de fleste sporingssamlere er mærkningsfasen, hvilket indebærer et ventetid, før man kan genvinde ikke-refereret hukommelse.

Sporingssamlere bruges mest til hukommelsesstyring på dynamiske sprog; de er langt den mest almindelige for Java-sproget og har været kommercielt bevist i produktionsmiljøer i mange år. Jeg vil fokusere på at spore samlere resten af ​​denne artikel, begyndende med nogle af de algoritmer, der implementerer denne tilgang til affaldsindsamling.

Sporing af samleralgoritmer

Kopiering og mærke-og-feje affaldsindsamling er ikke nyt, men de er stadig de to mest almindelige algoritmer, der implementerer sporing af affaldssamling i dag.

Kopiering af samlere

Traditionelle kopiersamlere bruger en fra-rummet og en til-plads - det vil sige to separat definerede adresserum i bunken. På tidspunktet for indsamling af affald kopieres de levende objekter inden for det område, der er defineret som fra-rum, til det næste ledige rum inden for det område, der er defineret som til-rum. Når alle de levende objekter i fra-rummet flyttes ud, kan hele fra-rummet genvindes. Når tildelingen begynder igen, starter den fra den første gratis placering i to-space.

I ældre implementeringer af denne algoritme placeres switch-fra-til-space-kontakten, hvilket betyder, at når to-space er fuld, udløses affaldssamling igen, og to-space bliver fra-space, som vist i figur 1.

Mere moderne implementeringer af kopieringsalgoritmen muliggør, at vilkårlige adresserum i bunken tildeles til rum og fra rum. I disse tilfælde behøver de ikke nødvendigvis at skifte placering med hinanden; snarere bliver hver en anden adresse plads i bunken.

En fordel ved kopiering af samlere er, at objekter fordeles tæt sammen i rummet, hvilket helt eliminerer fragmentering. Fragmentering er et almindeligt problem, som andre skraldopsamlingsalgoritmer kæmper med; noget, jeg vil diskutere senere i denne artikel.

Ulemper ved kopiering af samlere

Kopiering samlere er normalt stop-the-world samlere, hvilket betyder, at intet applikationsarbejde kan udføres, så længe affaldsindsamlingen er i cyklus. I en stop-the-world implementering, jo større område du har brug for at kopiere, jo større indflydelse får det på din applikations ydeevne. Dette er en ulempe for applikationer, der er følsomme over for responstid. Med en kopieringssamler skal du også overveje det værst tænkelige scenarie, når alt er live i fra-rummet. Du skal altid forlade nok plads til, at levende genstande kan flyttes, hvilket betyder, at rummet skal være stort nok til at være vært for alt i fra-rummet. Kopieringsalgoritmen er lidt hukommelseseffektiv på grund af denne begrænsning.

Mark-og-fej samlere

De fleste kommercielle JVM'er, der er implementeret i virksomhedens produktionsmiljøer, kører mark-and-sweep (eller markering) samlere, som ikke har den præstationseffekt, som kopiering af samlere har. Nogle af de mest berømte mærkesamlere er CMS, G1, GenPar og DeterministicGC (se Ressourcer).

EN mark-and-sweep-opsamler sporer referencer og markerer hvert fundet objekt med en "live" bit. Normalt svarer en sætbit til en adresse eller i nogle tilfælde et sæt adresser på bunken. Den levende bit kan for eksempel gemmes som en bit i objektets overskrift, en bitvektor eller et bit kort.

Efter at alt er blevet markeret live, vil sweep-fasen sparke ind. Hvis en samler har en sweep-fase, inkluderer den dybest set en eller anden mekanisme til at krydse bunken igen (ikke kun live-sæt, men hele bunlængden) for at lokalisere alle de ikke-markerede klumper af fortløbende hukommelsesadresserum. Umærket hukommelse er gratis og kan genvindes. Samleren forbinder derefter disse umærkede bidder til organiserede gratis lister. Der kan være forskellige gratis lister i en affaldssamler - normalt organiseret efter klumpstørrelser. Nogle JVM'er (som f.eks. JRockit Real Time) implementerer samlere med heuristikker, der dynamisk viser størrelsesintervallister baseret på applikationsprofileringsdata og objektstørrelsesstatistikker.

Når fejningsfasen er afsluttet, begynder tildelingen igen. Nye allokeringsområder tildeles fra de gratis lister, og hukommelsesstykker kan matches med objektstørrelser, gennemsnit af objektstørrelse pr. Tråd-ID eller de applikationsindstillede TLAB-størrelser. Tilpasning af ledig plads tættere på størrelsen af, hvad din applikation forsøger at allokere, optimerer hukommelsen og kan hjælpe med at reducere fragmentering.

Mere om TLAB-størrelser

TLAB- og TLA-partitionering (Thread Local Allocation Buffer eller Thread Local Area) diskuteres i JVM-ydeevneoptimering, del 1.

Ulemper ved mark-and-sweep samlere

Markeringsfasen afhænger af mængden af ​​live data på din bunke, mens fejningsfasen afhænger af bunkestørrelsen. Da du skal vente, indtil begge mærke og feje faser er komplette for at genvinde hukommelse, denne algoritme forårsager pausetidsudfordringer for større dynger og større live datasæt.

En måde, hvorpå du kan hjælpe stærkt hukommelsesforbrugende applikationer, er at bruge GC-tuningindstillinger, der passer til forskellige applikationsscenarier og behov. Tuning kan i mange tilfælde i det mindste hjælpe med at udskyde en af ​​disse faser fra at blive en risiko for din applikation eller serviceniveauaftaler (SLA'er). (En SLA specificerer, at applikationen vil opfylde bestemte responstider for applikationer - dvs. latens.) Tuning for hver belastningsændring og applikationsændring er en gentagen opgave, da indstillingen kun er gyldig for en bestemt arbejdsbelastning og tildelingshastighed.

Implementeringer af mark-and-sweep

Der er mindst to kommercielt tilgængelige og dokumenterede tilgange til implementering af mark-and-sweep-indsamling. Den ene er den parallelle tilgang og den anden er den samtidige (eller for det meste samtidige) tilgang.

Parallelle samlere

Parallel samling betyder, at ressourcer, der er tildelt processen, bruges parallelt til affaldsindsamling. De fleste kommercielt implementerede parallelle samlere er monolitiske stop-the-world samlere - alle applikationstråde stoppes, indtil hele affaldsindsamlingscyklussen er afsluttet. Ved at stoppe alle tråde kan alle ressourcer bruges effektivt parallelt til at afslutte affaldssamlingen gennem mærke- og fejningsfaserne. Dette fører til et meget højt effektivitetsniveau, hvilket normalt resulterer i høje score på gennemstrømningsbenchmarks som SPECjbb. Hvis kapacitet er afgørende for din applikation, er den parallelle tilgang et glimrende valg.