Komprimerede OOP'er i JVM

1. Oversigt

JVM styrer hukommelsen for os. Dette fjerner hukommelsesstyringsbyrden fra udviklerne, så vi behøver ikke at manipulere objektmarkører manuelt, hvilket har vist sig at være tidskrævende og fejlbehæftet.

Under emhætten indeholder JVM en masse smarte tricks til at optimere hukommelsesstyringsprocessen. Et trick er brugen af Komprimerede markører, som vi skal evaluere i denne artikel. Lad os først se, hvordan JVM repræsenterer objekter ved kørsel.

2. Runtime-objektrepræsentation

HotSpot JVM bruger en datastruktur kaldet oops eller Almindelige objektmarkører at repræsentere objekter. Disse Ups svarer til indfødte C-markører. Det instansOops er en særlig slags oop der repræsenterer objektforekomsterne i Java. Desuden understøtter JVM også en håndfuld andre Ups der holdes i OpenJDK-kildetræet.

Lad os se, hvordan JVM lægger sig ud instansOops i hukommelsen.

2.1. Objekthukommelseslayout

Hukommelseslayoutet til en instansOop er enkelt: det er bare objektoverskriften straks efterfulgt af nul eller flere referencer til instansfelter.

JVM-repræsentationen af ​​et objektoverskrift består af:

  • Et mærkeord tjener mange formål såsom Forudindtaget låsning, Identitets Hash værdier, og GC. Det er ikke et oop, men af ​​historiske grunde ligger det i OpenJDK'erne oop kildetræ. Mærkeordtilstanden indeholder også kun en uintptr_t, derfor, dens størrelse varierer mellem 4 og 8 byte i henholdsvis 32-bit og 64-bit arkitekturer
  • Et, muligvis komprimeret, Klass-ord, som repræsenterer en markør til klassemetadata. Før Java 7 pegede de på Permanent generation, men fra Java 8 og fremad peger de på Metaspace
  • Et 32-bit hul for at håndhæve objektjustering. Dette gør layoutet mere hardware venligt, som vi vil se senere

Umiddelbart efter overskriften skal der være nul eller flere referencer til instansfelter. I dette tilfælde a ord er et oprindeligt maskinord, så 32-bit på ældre 32-bit-maskiner og 64-bit på mere moderne systemer.

Objektoverskriften på arrays indeholder ud over markerings- og klass-ord et 32-bit-ord, der repræsenterer dets længde.

2.2. Anatomi af affald

Antag, at vi skifter fra en ældre 32-bit arkitektur til en mere moderne 64-bit maskine. I starten kan vi forvente at få et øjeblikkeligt præstationsforøg. Det er dog ikke altid tilfældet, når JVM er involveret.

Den største synder for denne mulige forringelse af ydeevnen er 64-bit objektreferencer. 64-bit referencer optager dobbelt så meget som 32-bit referencer, så dette fører til mere hukommelsesforbrug generelt og hyppigere GC-cyklusser. Jo mere tid der er afsat til GC-cyklusser, jo færre CPU-udførelsesskiver til vores applikationstråde.

Så skal vi skifte tilbage og bruge disse 32-bit arkitekturer igen? Selvom dette var en mulighed, kunne vi ikke have mere end 4 GB bunkeplads i 32-bit procesrum uden lidt mere arbejde.

3. Komprimerede OOP'er

Som det viser sig, kan JVM undgå at spilde hukommelse ved at komprimere objektmarkørerne eller Ups, så vi kan få det bedste fra begge verdener: tillader mere end 4 GB bunkeplads med 32-bit referencer i 64-bit maskiner!

3.1. Grundlæggende optimering

Som vi så tidligere, tilføjer JVM polstring til objekterne, så deres størrelse er et multiplum på 8 byte. Med disse polstringer er de sidste tre bits inde Ups er altid nul. Dette skyldes, at tal, der er et multiplum af 8, altid ender på 000 i binær.

Da JVM allerede ved, at de sidste tre bits altid er nul, er der ingen mening i at gemme disse ubetydelige nuller i bunken. I stedet antager det, at de er der og gemmer 3 andre mere betydningsfulde bits, som vi ikke tidligere kunne passe ind i 32-bits. Nu har vi en 32-bit adresse med 3 nøgler, der er skiftet til højre, så vi komprimerer en 35-bit markør til en 32-bit. Dette betyder, at vi kan bruge op til 32 GB - 232 + 3 = 235 = 32 GB - bunkeplads uden at bruge 64-bit referencer.

For at få denne optimering til at fungere, når JVM skal finde et objekt i hukommelsen det skifter markøren til venstre med 3 bit (tilføjer dybest set disse 3-nuller tilbage til slutningen). På den anden side skifter JVM markøren til højre med 3 bit, når du lægger en markør til bunken, for at kassere de tidligere tilføjede nuller. Grundlæggende udfører JVM lidt mere beregning for at spare plads. Heldigvis er bit shifting en virkelig triviel operation for de fleste CPU'er.

At muliggøre oop kompression, kan vi bruge -XX: + UseCompressedOops tuning flag. Det oop komprimering er standardadfærd fra og med Java 7, når den maksimale bunke størrelse er mindre end 32 GB. Når den maksimale dyngestørrelse er mere end 32 GB, slukker JVM automatisk for oop kompression. Så brug af hukommelse ud over en 32 Gb bunke størrelse skal styres forskelligt.

3.2. Ud over 32 GB

Det er også muligt at bruge komprimerede markører, når Java-dyngestørrelser er større end 32 GB. Selvom standardjusteringen af ​​objekter er 8 byte, kan denne værdi konfigureres ved hjælp af -XX:ObjectAlignmentInBytes tuning flag. Den angivne værdi skal have en effekt på to og skal være inden for området 8 og 256.

Vi kan beregne den maksimale mulige dyngestørrelse med komprimerede markører som følger:

4 GB * ObjectAlignmentInBytes

For eksempel, når objektjusteringen er 16 byte, kan vi bruge op til 64 GB bunkeplads med komprimerede markører.

Bemærk, at efterhånden som justeringsværdien stiger, kan det ubrugte mellemrum mellem objekter muligvis også øges. Som et resultat indser vi muligvis ikke nogen fordele ved at bruge komprimerede markører med store Java-bunkestørrelser.

3.3. Futuristiske GC'er

ZGC, en ny tilføjelse i Java 11, var en eksperimentel og skalerbar affaldssamler med lav latens.

Det kan håndtere forskellige intervaller af dyngestørrelser, mens GC-pauserne holdes under 10 millisekunder. Da ZGC skal bruge 64-bit farvede markører, det understøtter ikke komprimerede referencer. Så ved at bruge en ultra-lav latens GC som ZGC skal der vejes mod at bruge mere hukommelse.

Fra og med Java 15 understøtter ZGC komprimerede klassemarkører, men mangler stadig support til komprimerede OOP'er.

Alle nye GC-algoritmer udveksler dog ikke hukommelse for at være lav latenstid. For eksempel understøtter Shenandoah GC komprimerede referencer ud over at være en GC med lave pausetider.

Desuden er både Shenandoah og ZGC færdigbehandlet fra Java 15.

4. Konklusion

I denne artikel beskrev vi en JVM-hukommelsesstyringsproblem i 64-bit arkitekturer. Vi kiggede på komprimerede markører og objektjustering, og vi så, hvordan JVM kan løse disse problemer, så vi kan bruge større bunkestørrelser med mindre spildte pegepinde og et minimum af ekstra beregning.

For en mere detaljeret diskussion om komprimerede referencer anbefales det stærkt at tjekke endnu et stort stykke fra Aleksey Shipilëv. Også for at se, hvordan objektallokering fungerer inde i HotSpot JVM, skal du tjekke Memory Layout of Objects i Java-artiklen.


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