Bevezetés a ZGC-be: skálázható és kísérleti alacsony késésű JVM szemétgyűjtő

1. Bemutatkozás

Ma nem ritka, hogy az alkalmazások több ezer vagy akár több millió felhasználót szolgálnak ki egyidejűleg. Az ilyen alkalmazásokhoz óriási memória szükséges. Mindazon memória kezelése azonban könnyen befolyásolhatja az alkalmazás teljesítményét.

A probléma megoldására a Java 11 bevezette a Z Garbage Collector (ZGC) kísérleti szemétgyűjtő (GC) megvalósítását.

Ebben az oktatóanyagban meglátjuk hogyan képes a ZGC alacsony szünetidőket tartani még több terabájtos halmokon is.

2. Fogalmak

A ZGC működésének megértéséhez meg kell értenünk a memóriakezelés és a szemétgyűjtők mögött álló alapvető fogalmakat és terminológiát.

2.1. Memóriakezelés

A fizikai memória az a RAM, amelyet hardverünk biztosít.

Az operációs rendszer (OS) minden alkalmazáshoz virtuális memóriaterületet rendel.

Természetesen, virtuális memóriát tárolunk a fizikai memóriában, és az operációs rendszer felelős a kettő közötti leképezés fenntartásáért. Ez a leképezés általában hardveres gyorsítást jelent.

2.2. Többszörös leképezés

A többszörös leképezés azt jelenti, hogy a virtuális memóriában vannak meghatározott címek, amelyek a fizikai memóriában ugyanarra a címre mutatnak. Mivel az alkalmazások virtuális memórián keresztül férnek hozzá az adatokhoz, semmit sem tudnak erről a mechanizmusról (és nem is kell).

Eredményesen a virtuális memória több tartományát hozzárendeljük a fizikai memória azonos tartományához:

Első pillantásra a felhasználási esetek nem nyilvánvalóak, de később meglátjuk, hogy a ZGC-nek szüksége van rá varázslatához. Bizonyos biztonságot nyújt, mivel elválasztja az alkalmazások memóriatereit.

2.3. Áthelyezés

Mivel dinamikus memória-allokációt használunk, egy átlagos alkalmazás memóriája idővel széttöredezik. Ez azért van, mert amikor felszabadítunk egy tárgyat a memória közepén, ott egy szabad hely marad. Idővel ezek a rések felhalmozódnak, és emlékezetünk olyan lesz, mint egy sakktábla, amely a szabad és a felhasznált tér váltakozó területeiből készül.

Természetesen megpróbálhatnánk új tárgyakkal pótolni ezeket a hiányosságokat. Ehhez szabad memóriát kell átkutatnunk, amely elég nagy ahhoz, hogy elférjen az objektumunkon. Ennek elvégzése drága művelet, különösen, ha minden alkalommal meg kell csinálnunk, amikor memóriát akarunk lefoglalni. Ezenkívül a memória továbbra is széttöredezett lesz, mivel valószínűleg nem találunk szabad helyet, amely pontosan a szükséges méretet képviseli. Ezért az objektumok között hézagok lesznek. Természetesen ezek a rések kisebbek. Megpróbálhatjuk minimalizálni ezeket a hézagokat is, de még több feldolgozási teljesítményt használ fel.

A másik stratégia az, hogy gyakran a töredezett memóriaterületekről az objektumokat kompaktabb formátumban szabad területekre helyezheti át. A hatékonyság érdekében blokkokra osztjuk a memóriaterületet. Minden objektumot áthelyezünk egy blokkba, vagy egyiket sem. Így a memória lefoglalása gyorsabb lesz, mivel tudjuk, hogy a memóriában egész üres blokkok vannak.

2.4. Szemétgyüjtés

Java alkalmazás létrehozásakor nem kell felszabadítanunk a kiosztott memóriát, mert a szemétszedők ezt megteszik helyettünk. Összefoglalva, A GC figyeli, hogy mely objektumokhoz érhetünk el az alkalmazásunkból a referenciák láncolatán keresztül, és felszabadítja azokat, amelyeket nem érünk el.

A GC-nek munkája elvégzéséhez nyomon kell követnie a halomtérben lévő objektumok állapotát. Például elérhető egy lehetséges állapot. Ez azt jelenti, hogy az alkalmazás hivatkozást tartalmaz az objektumra. Ez a hivatkozás lehet tranzitív. Csak az számít, hogy az alkalmazás hivatkozások útján férhet hozzá ezekhez az objektumokhoz. Egy másik példa véglegesíthető: olyan objektumok, amelyekhez nem férhetünk hozzá. Ezeket a tárgyakat szemétnek tekintjük.

Ennek elérése érdekében a szemétszedőknek több fázisa van.

2.5. GC fázis tulajdonságai

A GC fázisoknak különböző tulajdonságai lehetnek:

  • a párhuzamos fázis több GC szálon is futhat
  • a sorozatszám fázis egyetlen szálon fut
  • a stop-the-world fázis nem futhat egyidejűleg az alkalmazás kódjával
  • a egyidejű szakasz futhat a háttérben, míg alkalmazásunk elvégzi a munkáját
  • an járulékos szakasz befejezhetõ, mielõtt minden munkáját befejezné, és késõbb folytathatja

Vegye figyelembe, hogy a fenti technikák mindegyikének megvannak az erősségei és gyengeségei. Tegyük fel például, hogy van egy fázisunk, amely egyidejűleg futtatható az alkalmazásunkkal. Ennek a fázisnak a soros megvalósítása a CPU teljes teljesítményének 1% -át igényli, és 1000 ms-ig fut. Ezzel szemben a párhuzamos megvalósítás a CPU 30% -át használja fel, és 50 ms alatt fejezi be a munkáját.

Ebben a példában a A párhuzamos megoldás összességében több CPU-t használ, mert összetettebb lehet, és szinkronizálnia kell a szálakat. Nehéz CPU-alkalmazások (például kötegelt feladatok) esetében ez problémát jelent, mivel kevesebb számítási erőnk van hasznos munkák elvégzésére.

Természetesen ez a példa kitalált számokkal rendelkezik. Egyértelmű azonban, hogy minden alkalmazásnak megvan a sajátossága, ezért eltérő a GC követelménye.

Részletesebb leírásokért olvassa el a Java memóriakezelésről szóló cikkünket.

3. ZGC fogalmak

A ZGC a világ leállításának fázisait a lehető legrövidebb időn belül kívánja biztosítani. Olyan módon éri el, hogy ezeknek a szüneteknek az időtartama nem nő a kupac méretével. Ezek a jellemzők teszik a ZGC-t jól illeszkedővé a kiszolgálóalkalmazásokhoz, ahol gyakran nagy halmok fordulnak elő, és a gyors válaszidő szükséges.

A kipróbált GC technikák mellett a ZGC új fogalmakat vezet be, amelyeket a következő szakaszokban tárgyalunk.

De most nézzük meg a ZGC működésének átfogó képét.

3.1. Nagy kép

A ZGC rendelkezik egy fázissal, amelyet jelölésnek hívnak, ahol megtaláljuk az elérhető objektumokat. A GC objektumállapot-információkat többféleképpen tárolhat. Például létrehozhatunk egy Térkép, ahol a kulcsok memória címek, és az érték az objektum állapota az adott címen. Ez egyszerű, de további memóriára van szükség az információk tárolásához. Ezenkívül egy ilyen térkép fenntartása is kihívást jelenthet.

A ZGC más megközelítést alkalmaz: a referencia állapotot tárolja a referencia bitjeiként. Referencia színezésnek hívják. De így új kihívás előtt állunk. A referencia bitjeinek beállítása az objektum metaadatainak tárolásához azt jelenti, hogy több hivatkozás is ugyanarra az objektumra mutathat, mivel az állapotbitek nem tartalmaznak információt az objektum helyéről. Multimapping a mentéshez!

Csökkenteni szeretnénk a memória töredezettségét is. A ZGC ennek érdekében az áthelyezést használja. De nagy halommal az áthelyezés lassú folyamat. Mivel a ZGC nem akar hosszú szünetidőket, az alkalmazással párhuzamosan az áthelyezés nagy részét megteszi. De ez új problémát vet fel.

Tegyük fel, hogy van utalásunk egy objektumra. A ZGC áthelyezi, és kontextus váltás történik, ahol az alkalmazás szál fut, és megpróbálja elérni ezt az objektumot a régi címén keresztül. A ZGC ennek megoldásához terhelési korlátokat használ. A teherzáró egy olyan kóddarab, amely akkor fut, amikor egy szál referenciát tölt be a kupacból - például amikor egy objektum nem primitív mezőjéhez férünk hozzá.

A ZGC-ben a terheléskorlátok ellenőrzik a referencia metaadatbitjeit. Ezektől a bitektől függően A ZGC végezhet némi feldolgozást a referencián, mielőtt megszereznénk. Ezért teljesen más referenciát hozhat létre. Ezt újrateremtésnek hívjuk.

3.2. Jelzés

A ZGC három fázisra bontja a jelölést.

Az első szakasz a világ megállása. Ebben a szakaszban keressük a gyökér referenciákat és jelöljük meg őket. A gyökér referenciák jelentik a kiindulási pontokat a halomban lévő objektumok eléréséhezpéldául helyi változók vagy statikus mezők. Mivel a gyökérreferenciák száma általában kicsi, ez a szakasz rövid.

A következő szakasz egyidejű. Ebben a szakaszban bejárjuk az objektumgrafikont, a gyökérreferenciákból kiindulva. Jelölünk minden tárgyat, amelyhez eljutunk. Továbbá, ha a teherzáró jelöletlen referenciát észlel, akkor azt is jelöli.

Az utolsó szakasz egyben a világ leállításának fázisa is néhány éles eset kezelésére, mint például a gyenge referenciák.

Ezen a ponton tudjuk, hogy mely tárgyakat érhetjük el.

A ZGC a jelölt0 és megjelölt1 metaadatbitek a jelöléshez.

3.3. Referencia színezés

A referencia a bájt helyzetét jelenti a virtuális memóriában. Ehhez azonban nem feltétlenül kell használnunk minden hivatkozási bitet - egyes bitek a referencia tulajdonságait jelenthetik. Ezt hívjuk referencia színezésnek.

32 bittel 4 gigabájtot tudunk megszólítani. Mivel manapság széles körben elterjedt, hogy a számítógépnek ennél több memóriája van, nyilvánvalóan nem használhatjuk ezeknek a 32 biteknek a színezéséhez. Ezért a ZGC 64 bites referenciákat használ. Azt jelenti A ZGC csak 64 bites platformokon érhető el:

A ZGC hivatkozások 42 bitet használnak a cím képviseletére. Ennek eredményeként a ZGC referenciák 4 terabájt memóriaterületet tudnak megcímezni.

Ezen felül 4 bitünk van a referenciaállapotok tárolására:

  • véglegesíthető bit - az objektum csak véglegesítőn keresztül érhető el
  • újratervezni bit - a hivatkozás naprakész és az objektum aktuális helyére mutat (lásd: áthelyezés)
  • jelölt0 és megjelölt1 bitek - ezek az elérhető objektumok jelölésére szolgálnak

Ezeket a biteket metaadat-biteknek is neveztük. A ZGC-ben pontosan egy ilyen metaadat-bit 1.

3.4. Áthelyezés

A ZGC-ben az áthelyezés a következő szakaszokból áll:

  1. Egyidejű fázis, amely blokkokat keres, át akarunk költözni és az áthelyezési halmazba helyezzük őket.
  2. A világ leállítása fázis áthelyezi az áthelyezési halmaz összes gyökér referenciáját, és frissíti hivatkozásaikat.
  3. Egyidejű fázis áthelyezi az áthelyezési halmaz összes megmaradt objektumát, és az átirányítási táblázatban tárolja a régi és új címek közötti leképezést.
  4. A fennmaradó referenciák átírása a következő jelölési szakaszban történik. Így nem kell kétszer bejárnunk az objektumfát. Alternatív megoldásként teherzárók is megtehetik.

3.5. Újratervezés és teherkorlátok

Ne feledje, hogy az áthelyezési szakaszban nem írtuk át a legtöbb hivatkozást az áthelyezett címekre. Ezért ezeket a hivatkozásokat felhasználva nem férünk hozzá a kívánt objektumokhoz. Még rosszabb, hogy hozzáférhetnénk a szeméthez.

A ZGC terhelési korlátokat használ a probléma megoldására. A teherkorlátok az áthelyezett objektumokra mutató hivatkozásokat újrateremtésnek nevezett technikával rögzítik.

Amikor az alkalmazás betölt egy referenciát, elindítja a teherzárót, amely a következő lépéseket követve adja vissza a helyes referenciát:

  1. Ellenőrzi, hogy a újratervezni A bit értéke 1. Ha igen, ez azt jelenti, hogy a hivatkozás naprakész, ezért nyugodtan visszaadhatjuk.
  2. Ezután ellenőrizzük, hogy a hivatkozott objektum szerepel-e az áthelyezési halmazban, vagy sem. Ha nem, ez azt jelenti, hogy nem akartuk áthelyezni. Annak elkerülése érdekében, hogy ezt az ellenőrzést legközelebb betöltsük, megadjuk a újratervezni bit 1-re és adja vissza a frissített referenciát.
  3. Most már tudjuk, hogy az az objektum, amelyhez hozzáférni akarunk, az áthelyezés célpontja volt. A kérdés csak az, hogy megtörtént-e az áthelyezés vagy sem? Ha az objektumot áthelyezték, ugorunk a következő lépésre. Ellenkező esetben most áthelyezzük, és létrehozunk egy bejegyzést az átirányítási táblában, amely tárolja az új áthelyezett objektumok új címét. Ezt követően folytatjuk a következő lépéssel.
  4. Most már tudjuk, hogy az objektumot áthelyezték. Vagy a ZGC, mi az előző lépésben, vagy a teherzáró az objektum egy korábbi találatakor. Frissítjük ezt a hivatkozást az objektum új helyére (vagy az előző lépés címével, vagy úgy, hogy megkeresjük az átirányítási táblázatban), beállítjuk a újratervezni bit, és adja vissza a referenciát.

És ennyi, a fenti lépésekkel biztosítottuk, hogy minden egyes alkalommal, amikor megpróbálunk hozzáférni egy objektumhoz, megkapjuk a legfrissebb hivatkozást arra. Mivel minden alkalommal, amikor referenciát töltünk be, ez kiváltja a teherzárót. Ezért csökkenti az alkalmazás teljesítményét. Különösen akkor, amikor először hozzáférünk egy áthelyezett tárgyhoz. De ezt az árat kell fizetnünk, ha rövid szünetidőket akarunk. Mivel ezek a lépések viszonylag gyorsak, nem befolyásolja jelentősen az alkalmazás teljesítményét.

4. Hogyan lehet engedélyezni a ZGC-t?

Alkalmazásunk futtatásakor a következő parancssori lehetőségekkel engedélyezhetjük a ZGC-t:

-XX: + UnlockExperimentalVMOptions -XX: + UseZGC

Ne feledje, hogy mivel a ZGC egy kísérleti GC, eltart egy ideig, amíg hivatalosan támogatják.

5. Következtetés

Ebben a cikkben láttuk, hogy a ZGC támogatni kívánja a nagy kupacméreteket, alacsony alkalmazási szünetidőkkel.

E cél elérése érdekében technikákat alkalmaz, beleértve a színes 64 bites referenciákat, a terheléskorlátokat, az áthelyezést és az újratérképezést.