Komprimerte OOP-er i JVM

1. Oversikt

JVM administrerer minnet for oss. Dette fjerner minnestyringsbyrden fra utviklerne, altså vi trenger ikke å manipulere objektpekere manuelt, som har vist seg å være tidkrevende og utsatt for feil.

Under panseret inneholder JVM mange smarte triks for å optimalisere prosessen med minnehåndtering. Ett triks er bruken av Komprimerte pekere, som vi skal evaluere i denne artikkelen. Først og fremst, la oss se hvordan JVM representerer objekter ved kjøretid.

2. Runtime Object Representation

HotSpot JVM bruker en datastruktur som heter oops eller Vanlige pekere å representere objekter. Disse oops tilsvarer innfødte C-pekere. De instansOops er en spesiell type oop som representerer objektforekomstene i Java. Videre støtter JVM også en håndfull andre oops som holdes i OpenJDK-kildetreet.

La oss se hvordan JVM legger ut instansOops i minnet.

2.1. Objektminnelayout

Minneoppsettet til en instansOop er enkelt: det er bare objektoverskriften umiddelbart etterfulgt av null eller flere referanser til forekomstfelt.

JVM-representasjonen av et objektoverskrift består av:

  • Ett merkeord tjener mange formål som Forutinntatt låsing, Identitetshashverdier, og GC. Det er ikke en oop, men av historiske grunner ligger det i OpenJDK oop kildetre. I tillegg inneholder merkeordtilstanden bare en uintptr_t, derfor, størrelsen varierer mellom 4 og 8 byte i henholdsvis 32-biters og 64-biters arkitekturer
  • Ett, muligens komprimert, Klass-ord, som representerer en peker til klassemetadata. Før Java 7 pekte de på Permanent generasjon, men fra Java 8 og utover, peker de på Metaspace
  • Et 32-bit gap for å håndheve justering av objekter. Dette gjør oppsettet mer maskinvarevennlig, som vi vil se senere

Rett etter overskriften skal det være null eller flere referanser til forekomstfelt. I dette tilfellet, a ord er et innfødt maskinord, så 32-bit på eldre 32-bits maskiner og 64-bit på mer moderne systemer.

Objektoverskriften på matriser, i tillegg til merke- og klassord, inneholder et 32-biters ord som representerer lengden.

2.2. Anatomi av avfall

Anta at vi skal bytte fra en eldre 32-biters arkitektur til en mer moderne 64-biters maskin. Først kan vi forvente å få et øyeblikkelig ytelsesløft. Imidlertid er det ikke alltid tilfelle når JVM er involvert.

Den viktigste skyldige for denne mulige ytelsesforringelsen er 64-biters objektreferanser. 64-biters referanser tar dobbelt så mye plass som 32-biters referanser, så dette fører til mer minneforbruk generelt og hyppigere GC-sykluser. Jo mer tid som er tildelt GC-sykluser, jo færre CPU-kjøringsskiver for applikasjonstrådene våre.

Så, bør vi bytte tilbake og bruke disse 32-biters arkitekturer igjen? Selv om dette var et alternativ, kunne vi ikke ha mer enn 4 GB dyngplass i 32-biters prosessrom uten litt mer arbeid.

3. Komprimerte OOP-er

Som det viser seg kan JVM unngå å kaste bort minne ved å komprimere objektpekerne eller oops, slik at vi kan få det beste fra begge verdener: tillater mer enn 4 GB masse plass med 32-bit referanser i 64-bit maskiner!

3.1. Grunnleggende optimalisering

Som vi så tidligere, legger JVM til polstring til objektene slik at størrelsen deres er et multiplum på 8 byte. Med disse polstringene, de tre siste bitene inn oops er alltid null. Dette er fordi tall som er et multiplum av 8 alltid ender på 000 i binær.

Siden JVM allerede vet at de tre siste bitene alltid er null, er det ingen vits i å lagre de ubetydelige nullene i dyngen. I stedet antar det at de er der og lagrer 3 andre mer betydningsfulle biter som vi ikke tidligere kunne passe inn i 32-bits. Nå har vi en 32-biters adresse med 3 høyre-skiftede nuller, så vi komprimerer en 35-bit peker til en 32-bit. Dette betyr at vi kan bruke opptil 32 GB - 232 + 3 = 235 = 32 GB - masse plass uten å bruke 64-bit referanser.

For å få denne optimaliseringen til å fungere når JVM trenger å finne et objekt i minnet den forskyver pekeren til venstre med 3 biter (legger i utgangspunktet de 3 nuller tilbake til slutten). På den annen side, når du legger en peker til dyngen, forskyver JVM pekeren til høyre med 3 biter for å forkaste de tidligere lagt til nuller. I utgangspunktet utfører JVM litt mer beregning for å spare plass. Heldigvis er bit shifting en veldig triviell operasjon for de fleste CPUer.

For å aktivere oop komprimering, kan vi bruke -XX: + UseCompressedOops innstillingsflagg. De oop komprimering er standard oppførsel fra Java 7 og utover når den maksimale dyngdestørrelsen er mindre enn 32 GB. Når den maksimale dyngstørrelsen er mer enn 32 GB, vil JVM automatisk slå av oop komprimering. Så minneutnyttelse utover en 32 Gb haugstørrelse må styres annerledes.

3.2. Utover 32 GB

Det er også mulig å bruke komprimerte pekere når Java-haugestørrelser er større enn 32 GB. Selv om standardobjektjusteringen er 8 byte, kan denne verdien konfigureres ved hjelp av -XX:ObjectAlignmentInBytes innstillingsflagg. Den angitte verdien skal være en kraft på to og må være i området 8 og 256.

Vi kan beregne den maksimale mulige dyngestørrelsen med komprimerte pekere på følgende måte:

4 GB * ObjectAlignmentInBytes

For eksempel, når objektjusteringen er 16 byte, kan vi bruke opptil 64 GB masse plass med komprimerte pekere.

Vær oppmerksom på at når justeringsverdien øker, kan også det ubrukte rommet mellom objekter øke. Som et resultat kan det hende at vi ikke oppdager noen fordeler ved å bruke komprimerte pekere med store Java-dyngstørrelser.

3.3. Futuristiske GC-er

ZGC, et nytt tillegg i Java 11, var en eksperimentell og skalerbar søppelsamler med lav latens.

Den kan håndtere forskjellige serier med dyngestørrelser mens GC-pausene holdes under 10 millisekunder. Siden ZGC må bruke 64-biters fargede pekere, den støtter ikke komprimerte referanser. Så, ved bruk av en ultra-lav latens-GC som ZGC, må det veies mot å bruke mer minne.

Fra og med Java 15 støtter ZGC komprimerte klassepekere, men mangler fortsatt støtte for komprimerte OOP-er.

Alle nye GC-algoritmer vil imidlertid ikke bytte minne for å være lavforsinket. For eksempel støtter Shenandoah GC komprimerte referanser i tillegg til å være en GC med lave pausetider.

Videre er både Shenandoah og ZGC ferdigstilt fra og med Java 15.

4. Konklusjon

I denne artikkelen beskrev vi a JVM-minnehåndteringsproblem i 64-biters arkitekturer. Vi så på komprimerte pekere og objektjustering, og vi så hvordan JVM kan løse disse problemene, slik at vi kan bruke større dyngstørrelser med mindre sløsende pekere og et minimum av ekstra beregning.

For en mer detaljert diskusjon om komprimerte referanser, anbefales det sterkt å sjekke ut enda et flott stykke fra Aleksey Shipilëv. Hvis du vil se hvordan objektallokering fungerer inne i HotSpot JVM, kan du sjekke ut Memory Layout of Objects i Java-artikkelen.


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