Tömörített OOP-k a JVM-ben

1. Áttekintés

A JVM kezeli a memóriát számunkra. Ez eltávolítja a memóriakezelési terhet a fejlesztőktől, így nem kell manuálisan manipulálnunk az objektummutatókat, amely bizonyítottan időigényes és hibára hajlamos.

A motorháztető alatt a JVM sok remek trükköt tartalmaz a memóriakezelési folyamat optimalizálása érdekében. Az egyik trükk a használata Tömörített mutatók, amelyet ebben a cikkben értékelünk. Először nézzük meg, hogy a JVM hogyan ábrázolja az objektumokat futás közben.

2. Futásidejű objektumábrázolás

A HotSpot JVM egy úgynevezett adatszerkezetet használ hoppás vagy Közönséges tárgymutatók tárgyakat ábrázolni. Ezek hoppá egyenértékűek a natív C mutatókkal. A instanceOops egy különleges fajtája hoppá amely a Java objektum példányait reprezentálja. Sőt, a JVM egy maroknyi más támogatást is nyújt hoppá amelyeket az OpenJDK forrásfájában tárolnak.

Lássuk, hogyan helyezkedik el a JVM instanceOops a memóriában.

2.1. Objektum memória elrendezése

A memória elrendezése instanceOop egyszerű: ez csak az objektumfejléc, amelyet közvetlenül követ a nulla vagy több hivatkozás a példánymezőkre.

Az objektumfejléc JVM-ábrázolása a következőkből áll:

  • Egy jelszó sok célt szolgál, mint pl Elfogult zárolás, Identity Hash Értékek, és GC. Ez nem egy hopp, de történelmi okokból az OpenJDK-ban található hoppá forrásfa. Ezenkívül az állam védjegy csak a-t tartalmaz uintptr_t, ebből kifolyólag, mérete 4 és 8 bájt között változik 32 bites és 64 bites architektúrákban
  • Egy, esetleg tömörített Klass szó, amely az osztály metaadatainak mutatóját jelenti. A Java 7 előtt rámutattak a Állandó Generáció, de a Java 8-tól kezdve a Metaspace
  • 32 bites rés az objektum igazításának kikényszerítésére. Ez hardverbarátabbá teszi az elrendezést, amint később látni fogjuk

Közvetlenül a fejléc után nulla vagy több hivatkozásnak kell lennie a példánymezőkre. Ebben az esetben a szó egy natív gépszó, tehát 32 bites a régi 32 bites gépeken és 64 bites a modernebb rendszereken.

A tömbök objektum fejlécében a mark és klass szavak mellett egy 32 bites szó is szerepel, amely a hosszát jelöli.

2.2. A hulladék anatómiája

Tegyük fel, hogy a régi 32 bites architektúráról egy modernebb 64 bites gépre fogunk váltani. Eleinte számíthatunk azonnali teljesítménynövekedésre. Ez azonban nem mindig áll fenn, amikor a JVM részt vesz.

A lehetséges teljesítményromlás legfőbb hibája a 64 bites objektumreferenciák. A 64 bites referenciák kétszer annyi helyet foglalnak el, mint a 32 bites referenciák, így ez általában nagyobb memóriafelhasználást és gyakoribb GC ciklusokat eredményez. Minél több időt fordítunk a GC ciklusokra, annál kevesebb CPU végrehajtási szelet van alkalmazásszálainkhoz.

Tehát vissza kellene kapcsolnunk és újra használnunk kellene ezeket a 32 bites architektúrákat? Még akkor is, ha ez opció lenne, nem rendelkezhetnénk 4 GB-nál több halomterülettel a 32 bites folyamatterekben, kicsit több munka nélkül.

3. Tömörített OOP-k

Mint kiderült, a JVM elkerülheti a memória pazarlását az objektummutatók vagy a hoppá, így mindkét világ legjobbja lehet: több mint 4 GB halomterület megengedése 32 bites referenciákkal a 64 bites gépekben!

3.1. Alapvető optimalizálás

Mint korábban láttuk, a JVM párnázatot ad az objektumokhoz, így méretük 8 bájt többszöröse. Ezekkel a párnázásokkal az utolsó három bit be hoppá mindig nulla. Ugyanis a 8-szoros számok mindig végződnek 000 bináris formában.

Mivel a JVM már tudja, hogy az utolsó három bit mindig nulla, nincs értelme ezeket a jelentéktelen nullákat tárolni a kupacban. Ehelyett azt feltételezi, hogy ott vannak, és tárol még 3 másik jelentős bitet, amelyekbe korábban nem tudtunk beleilleszteni a 32 bitet. Most van egy 32 bites címünk 3 jobbra eltolt nullával, ezért egy 35 bites mutatót egy 32 bitesre tömörítünk. Ez azt jelenti, hogy akár 32 GB - 232 + 3 = 235 = 32 GB - halomterületet is felhasználhatunk 64 bites referenciák használata nélkül.

Ennek az optimalizálásnak a működése érdekében, amikor a JVM-nek meg kell találnia egy objektumot a memóriában 3 bittel tolja balra a mutatót (alapvetően ezeket a 3 nullákat adja vissza a végére). Másrészt, amikor mutatót tölt a kupacba, a JVM 3 bittel jobbra tolja a mutatót, hogy eldobja azokat a korábban hozzáadott nullákat. Alapvetően a JVM egy kicsit több számítást végez, hogy helyet takarítson meg. Szerencsére a biteltolás valóban triviális művelet a legtöbb processzor számára.

Engedélyezni hoppá tömörítés, használhatjuk a -XX: + UseCompressedOops tuning zászló. A hoppá a tömörítés az alapértelmezett viselkedés a Java 7-től kezdődően, amikor a maximális kupacméret kisebb, mint 32 GB. Ha a maximális kupacméret meghaladja a 32 GB-ot, a JVM automatikusan kikapcsolja a hoppá tömörítés. Tehát a 32 Gb-os kupacméretet meghaladó memória-kihasználást másképp kell kezelni.

3.2. 32 GB-on felül

Tömörített mutatók is használhatók, ha a Java halom mérete nagyobb, mint 32 GB. Bár az alapértelmezett objektum igazítás 8 bájt, ez az érték a -XX:ObjectAlignmentInBytes tuning zászló. A megadott értéknek kettőnek kell lennie, és a 8 és 256 tartományon belül kell lennie.

A tömörített mutatókkal a következőképpen számíthatjuk ki a lehető legnagyobb halom méretet:

4 GB * ObjectAlignmentInBytes

Például, ha az objektum igazítása 16 bájt, akkor akár 64 GB halomterületet is felhasználhatunk tömörített mutatókkal.

Felhívjuk figyelmét, hogy az igazítási érték növekedésével az objektumok közötti kihasználatlan terület is növekedhet. Ennek eredményeként előfordulhat, hogy nem veszünk észre előnyöket a tömörített mutatók nagy Java halomméretekkel történő használatából.

3.3. Futurisztikus GC-k

A ZGC, a Java 11 új kiegészítése, kísérleti jellegű és méretezhető, alacsony késésű szemétgyűjtő volt.

Képes kezelni a halomméretek különböző tartományait, miközben a GC szüneteket 10 milliszekundum alatt tartja. Mivel a ZGC-nek 64 bites színes mutatókat kell használnia, nem támogatja a tömörített hivatkozásokat. Tehát egy olyan rendkívül alacsony késleltetésű GC használatával, mint a ZGC, mérlegelni kell a több memória használatát.

A Java 15-től kezdve a ZGC támogatja a tömörített osztálymutatókat, de még mindig hiányzik a tömörített OOP-k támogatása.

Az összes új GC algoritmus azonban nem fogja kicserélni a memóriát alacsony késleltetésűnek. Például a Shenandoah GC tömörített referenciákat támogat azon túl, hogy alacsony szünetidőkkel rendelkező GC.

Sőt, a Shenandoah és a ZGC is véglegesítve van a Java 15-től.

4. Következtetés

Ebben a cikkben leírtuk a JVM memóriakezelési probléma 64 bites architektúrákban. Megnéztük tömörített mutatók és objektum igazítás, és láttuk, hogy a JVM hogyan tudja kezelni ezeket a problémákat, lehetővé téve számunkra a nagyobb kupacméretek használatát, kevésbé pazarló mutatókkal és minimális extra számítással.

A tömörített referenciákról részletesebb vita érdekében ajánlott megnézni Aleksey Shipilëv újabb remek darabját. Ha meg szeretné tudni, hogyan működik az objektum-allokáció a HotSpot JVM-en belül, olvassa el az Objektumok memóriaelrendezése a Java-ban cikket.