G1 szemétgyűjtő

Talán az egyik leglátogatottabb poszt az oldalon a Java HotSpot virtuális gép szemétgyűjtő algoritmusairól szól. Abban a G1 szemétgyűjtő algoritmus friss mivolta folytán még nem került leírásra, azonban most került bejelentésre, hogy a JDK 9-ben már az lesz az alapértelmezett, ezért érdemes vele foglalkozni.

A G1-ben bár számos újdonság van, de alapvetően a többi szemétgyűjtő által is használt fogalmakkal dolgozik, így érdemes az említett posztot újra átolvasni.

Az egyik legjobb hozzáférhető cikk a témával kapcsolatban a Getting Started with the G1 Garbage Collector Oracle tutorial, mely képekkel illusztrálva írja le a G1 működését.

A legjobb, és egyetlen könyv Scott Oaks Java Performance, The Definitive Guide című könyve. Mivel 2014-ben jelent meg, ez az egyetlen up-to-date könyv a témában. Írója jelenleg architect az Oracle-nél, a middleware termékek teljesítményhangolásán dolgozik, 2001 óta a Java Performance group tagja. A könyv mindenkinek javasolt, aki a JVM teljesítményhangolásával szeretne foglalkozni, ugyanis részletesen tárgyalja a JIT-et, az összes szemétgyűjtő algoritmust, valamint rengeteg finomhangolási trükköt mutat, valamint iránymutatásokat, hogy hogyan fejlesszünk hatékony alkalmazásokat a következők használatával: Java SE, többszálúság, adatbázis hozzáférés, Java EE technológiák.

Scott Oaks: Java Performance, The Definitive Guide

A G1 szemétgyűjtő bemutatásáról egy prezentációt is készítettem, mely elérhető itt.

A G1 algoritmus a Java 7u4-től ajánlott élesben használni, előtte lévő JVM-ekben (6u20-tól) csak experimental funkció. Alapvetően szerver kategóriájú gépekre fejlesztették ki, melyekben több processzor(mag) van, és memória is bőven. Az alapvető cél, hogy növeljék a JVM-en futó alkalmazások áteresztőképességét (egységnyi idő alatt elvégzett műveletek száma), és csökkentsék a szemétgyűjtőkre jellemző leállást (stop the world), ráadásul megjósolhatóvá téve annak hosszát.

A G1 szemétgyűjtő négy fő műveletből áll, ezek további fázisokra bonthatók. Külön művelet végzi a young generation begyűjtését. A CMS szemétgyűjtőhöz hasonlóan bizonyos fázisok több szálon (parallel), bizonyos fázisok az alkalmazás futásával párhuzamosan futnak (concurrent). Bizonyos fázisok ugyanúgy leállítják az alkalmazás szálak működését, és kizárólag a szemétgyűjtő fut.

A G1 töredezettségmentesítést is végez működéséből adódóan. Míg a Parallel GC a teljes heapet tördezettségmentesíti, ez sokáig is tart, addig a CMS szemétgyűjtő viszont nem töredezettségmentesít.

Bizonyos esetekben lefuthat full GC is, amikor megáll az alkalmazás, és egy szálon megtörténik a teljes szemétgyűjtés, de a GC finomhangolásával ezt kell minimalizálni.

A G1 kis mértékben több CPU és heap igénnyel rendelkezik, de nem számottevően.

Nézzük, hogy ezeket a célokat hogyan éri el? A G1 a memóriát régiókra osztja, és bizonyos régiókat az eden, survivor vagy az old területhez rendeli. Így ezen memóriaterületek nem folytonosak, de felhasználásuk sokkal flexibilisebb. Nem osztja ki a teljes memóriát, vannak üres régiók is. A régiók száma valamilyen kettő hatvány, alapbeállításokkal próbálja a régiók számát 2048-on tartani, egy régió mérete 1-től 32Mb-ig terjedhet.

A G1 a legtöbb szemetet tartalmazó régiókat üríti. Innen ered a neve is, Garbage First. Kiürítésre annyi régiót választ, hogy be tudja tartani azt a célt, mely megadja, hogy mennyit állhat az alkalmazás (user defined pause time target). Az, hogy mennyi régiót tud mennyi idő alatt felszabadítani, az előző futási idők alapján becsli.

Ürítésre evakuálást használ, azaz a még élő objektumokat más régiókba másolja. A régió így felszabadul. Valamint automatikusan megtörténik a töredezettségmentesítés is, hiszen a teljes régió újrafelhasználhatóvá válik, nem csak kis területek. Valamint a JVM így dinamikusan tudja méretezni az eden, survivor és old területeket régiók bevonásával vagy elvételével.

Két fogalmat érdemes még ismerni, a Remembered Set (RSet) régiónként létezik, és az ahhoz tartozó objektum referenciákat tartja nyilván. A Collection Sets (CSets) tartalmazza a GC által összegyűjtendő régiók hivatkozásait.

Akkor nézzük meg a G1 műveleteit:

  • Young collection
  • Background, concurrent cycle
  • Mixed collection
  • Full GC

A young gc akkor indul, ha az eden tele van, stop the world, a szemétgyűjtő az eden és survivor régiókban lévő élő objektumokat átpakolja a survivor régiókba, és az eden régiókat üríti. A survivor régiókban a megfelelő mennyiségű szemétgyűjtést túlélő objektumokat átpakolja az old régiókba. Természetesen itt is történhet olyan, hogy míg az edenből pakolja át az objektumokat a survivor területre, az megtelik. Ilyenkor a többi objektumot az old területre mozgatja. Futása után tehát az eden üres. Valamint újraszámolja az eden és survivor terület méretét, annak alapján, hogy mennyi ideig futott a szemétgyűjtő.

Az old generation szemétgyűjtése több fázisból áll, melyek lehetnek stop the world és concurrent fázisok is. Ezek a következők:

  • Initial mark
  • Root region scan
  • Concurrent mark
  • Remark
  • Cleanup
  • Concurrent cleanup

Első fázis a Initial marking. Stop the world, és mindig egy young gc fut előtte.

A következő fázis a Root region scan, ez az alkalmazással konkurens fut. Ennek futása közben nem futhat minor GC. Ha mégis szükség lenne, akkor a young GC bevárja, és csak utána fut.

A harmadik fázis a Concurrent mark, szintén konkurens fut. Ezt bármikor megszakíthatja egy young gc. Ez jelölgeti meg valójában a élő objektumokat.

Ezt követi a Remarking, majd a normál Cleanup fázis, mindkettő stop the world, de igen rövid időre. A Remarking véglegesíti az élő objektumok bejelölését. Ez egy snapshot-at-the-beginning (SATB) algoritmust használ, mely gyorsabb, mint a CMS-ben lévő.

Majd jön a Concurrent cleanup, mely párhuzamosan fut az alkalmazással.

Ezen művelet során azonban csak kevés memória szabadul fel, hatására csak megjelölődnek azon old régiók, melyek a legtöbb szemetet tartalmazzák, így a legtöbb memória szabadul fel, ha ráfut a szemétgyűjtő.

Ekkor fut le a Mixed collection. Mixed, mert lefut egy young collection is, és pár old régiót is felszabadít. Ezért stop the world, és utána az eden üres. Majd ez addig ismétlődik, míg (majdnem) mind tisztításra jelölt régiót fel nem szabadítja.

Eztán kezdődik minden elölről, young GC, valamint a Background, concurrent cycle.

Bizonyos esetekben azonban mégis le kell futnia a full GC-nek. Nézzük, hogy melyek ezek az esetek.

Az első a Concurrent mode failure. Concurrent mark közben betelik az old terület. Ekkor jön a stop the world, és a full GC.

A második a Promotion failure. A Mixed collection iterációk között telik meg az old gen, még mielőtt a Mixed collection elegendő régiót szabadított volna fel.

Evacuation failure történik, ha a young gc futtatásakor a survivor és az old gen is megtelik, így nem lehet hova menekíteni az edenből az objektumokat.

Humongous allocation failure történik, ha olyan nagyméretű objektumnak nem sikerül helyet foglalni, mely akár több régiót is elfoglalna. Ezeket amúgy Humongous objecteknek hívja. Itt a JVM-nek egymás után következő régiókat kell találnia, ha nem sikerül, akkor full GC-t futtat. Ráadásul, ha egy objektum nagyobb mint egy régió, a G1 collector esetén a JVM automatikusan az old gen-ben foglal helyet, mely takarítása ritkábban történik.

A G1 szemétgyűjtőt a -XX:+UseG1GC kapcsolóval lehet bekapcsolni. A leghatékonyabb módszer ennek finomhangolására azon érték beállítása, mely időt a szemétgyűjtőnek adunk, hogy a JVM mennyit állhat. Persze ez csak egy célszám, a JVM megpróbálja ezt betartani. A -XX:MaxGCPauseMillis=n kapcsolóval állíthatjuk, alapértelmezett értéke 200. A -XX:InitiatingHeapOccupancyPercent=n kapcsoló szintén nagyon fontos, ezzel tudjuk megadni, hogy a Background, concurrent cycle mikor induljon be. Alapértelmezett értéke 45, amely azt jelenti, hogy a heap 45%-os telítettségénél kezd elindulni. Ennek csökkentésével gyakoribbra tudjuk venni a GC futását.

Persze lehet még játszani azon paraméterekkel, melyek más szemétgyűjtőknél is adottak. Ilyenek pl. a heap mérete, a new és old gen méretének aránya, az eden és survivor területek méretének aránya. Ilyen hangolható paraméter az is, hogy az objektumok mennyi szemétgyűjtés után kerüljenek a survivor területről az old gen-be. Valamint állítható a párhuzamosan, valamint a konkurensen futó szálak száma is. De ezek egyike sem G1 specifikus.