JVM architektúra
ClassLoader, Runtime Data Areas, Heap, Stack és Metaspace
JVM Architektúra
A Java "Write Once, Run Anywhere" ígéretének motorja — az osztálybetöltéstől a bytecode-végrehajtáson át a memóriakezelésig.
1. Definíció
Mi ez?
A Java Virtual Machine (JVM) egy absztrakt számítógépes gép, amely futtatókörnyezetet biztosít a Java bytecode végrehajtásához. Közvetítő rétegként működik a lefordított Java programok (.class fájlok) és az operációs rendszer, illetve a hardver között.
Miért létezik?
A JVM Java Write Once, Run Anywhere (WORA) elvének megvalósítása érdekében jött létre. Egy Java program egyszer fordul le platformfüggetlen bytecode-dá; a célplatformon futó JVM ezt a bytecode-ot értelmezi vagy natív gépi utasításokká fordítja. Így nincs szükség platformonkénti újrafordításra.
Hol helyezkedik el?
Magas szintű végrehajtási folyamat:
- A Java forráskód (
.java) leforduljavacsegítségével. - Az eredmény a bytecode (
.class). - A platformon futó JVM ezt végrehajtja vagy tovább fordítja.
- A futás végül a natív OS / CPU szintjén történik.
A JVM a bytecode és a hardver között helyezkedik el. A JDK tartalmazza a fordítót (javac) és a JVM-et; a JRE csak a JVM-et és a standard könyvtárakat.
2. Alapfogalmak
2.1 A JVM architektúra áttekintése
A JVM fő építőelemei:
- ClassLoader subsystem — Bootstrap, Platform/Extension és Application loaderekkel tölti be az osztályokat.
- Runtime data areas — heap, metaspace, Java stackek, PC regiszterek és native stackek.
- Execution engine — interpreter, JIT compiler és garbage collector.
- JNI — kapcsolat a natív metódusok felé.
2.2 ClassLoader Subsystem
A ClassLoader subsystem felelős az osztályok betöltéséért, linkeléséért és inicializálásáért futásidőben.
ClassLoader hierarchia
| ClassLoader | Mit tölt be | Szülő |
|---|---|---|
| Bootstrap | java.lang.*, java.util.*, alap JDK osztályok (jrt:/) |
nincs (natív) |
| Platform (korábban Extension) | JDK extension modulok (java.se, jdk.*) |
Bootstrap |
| Application (System) | Az alkalmazás classpath-ján lévő osztályok (-cp) |
Platform |
| Custom | Felhasználói osztályforrások (jar, hálózat, adatbázis…) | Bármelyik fenti |
Delegálási modell (Parent-First)
A parent-first delegálás menete:
- Az Application ClassLoader először a szülőjétől kéri a
com.example.Foobetöltését. - A Platform ClassLoader továbbadja a kérést felfelé.
- A Bootstrap ClassLoader próbálkozik először, és ha nem találja, visszaadja a vezérlést lefelé.
- Az osztályt végül az a loader tölti be, amelyik tényleg megtalálja, gyakran az Application ClassLoader.
Minden classloader először a szülőjéhez delegál, mielőtt maga próbálná betölteni az osztályt. Ez megakadályozza, hogy a felhasználói kód véletlenül (vagy szándékosan) felülírja az alap JDK osztályokat.
Osztálybetöltési fázisok
| Fázis | Leírás |
|---|---|
| Loading (Betöltés) | A .class bináris fájl beolvasása és egy Class objektum létrehozása |
| Verification (Ellenőrzés) | Bytecode helyességének és biztonsági feltételek ellenőrzése |
| Preparation (Előkészítés) | Memória foglalása statikus mezőknek; alapértelmezett értékek beállítása |
| Resolution (Feloldás) | Szimbolikus hivatkozások felváltása direkt memóriahivatkozásokkal |
| Initialization (Inicializálás) | <clinit> végrehajtása (statikus inicializálók és statikus blokkok) |
2.3 Runtime Data Areas
Heap (minden thread között megosztott)
A heap az elsődleges memóriaterület objektum-allokációhoz, amelyet a Garbage Collector kezel.
A heap felosztása röviden:
Young Generation — itt indul a legtöbb új objektum.
Eden Space — ide kerülnek a friss allokációk.
Survivor 0 / Survivor 1 — az egy vagy több Minor GC-t túlélő objektumok ide kerülnek.
Old (Tenured) Generation — a hosszú életű, promotált objektumok helye.
Az objektumok először az Eden-ben kerülnek allokálásra.
Minor GC után a túlélő objektumok Survivor space-be kerülnek; elegendő túlélés után promoválódnak az Old Gen-be.
A Major/Full GC az egész heap-et összegyűjti.
Metaspace (Method Area — Java 8 óta)
Osztály-metaadatokat tárol: osztálystruktúrák, metódus bytecode, konstans pool, mező/metódus leírók, statikus változók.
- Java 8 előtt: PermGen-ben tárolták (rögzített, heap-en).
- Java 8 óta: Metaspace-ben (natív/off-heap memória — alapból dinamikusan nő).
| PermGen (≤ Java 7) | Metaspace (≥ Java 8) | |
|---|---|---|
| Elhelyezkedés | Java heap | Natív memória |
| Alapméret | Rögzített (~64–256 MB) | Korlátlan (rendszer RAM) |
| OOM oka | Túl sok osztály | Korlátlan natív növekedés |
| Vezérlő flag | -XX:MaxPermSize |
-XX:MaxMetaspaceSize |
Java Stacks (thread-enkénti)
Minden thread-nek saját Java Stack-je van, amely stack frame-eket tárol. Minden metódushíváskor egy új frame kerül a stackre, és a metódus visszatérésekor lekerül.
Egy stack frame tartalmaz:
- Local Variable Array — metódus argumentumok és lokális változók
- Operand Stack — bytecode utasítások munkaterülete
- Frame Data — hivatkozás a konstans poolra, visszatérési érték info
Példa stack állapotra:
- Thread-1 stack tartalmazhatja a
main()→foo()→bar()frame-eket. - Thread-2 stack ezzel párhuzamosan tarthat például egy
run()frame-et. - Minden thread saját stackkel rendelkezik; a frame-ek nem közösek.
Az alapértelmezett stack méret thread-enként ~512 KB–1 MB. Mély/végtelen rekurzió esetén StackOverflowError keletkezik.
PC (Program Counter) Registers (thread-enkénti)
Az egyes thread-ek aktuálisan végrehajtott bytecode utasításának címét tárolja. Natív metódusoknál nincs meghatározva.
Native Method Stacks (thread-enkénti)
A JNI-n keresztül meghívott natív (C/C++) metódusok végrehajtását támogatja. Elkülönül a Java stackektől.
2.4 Execution Engine
| Komponens | Szerepe |
|---|---|
| Interpreter | Bytecode utasításokat egyenként hajt végre; gyors indulás, lassú steady-state |
| JIT Compiler | Profiling alapján felismeri a "hot" metódusokat és natív kóddá fordítja; lassú warm-up, gyors végrehajtás |
| C1 Compiler | Gyors, könnyű optimalizálású JIT szint (client compiler) |
| C2 Compiler | Erős optimalizálású JIT szint (server compiler); hot path-ekhez |
| Garbage Collector | Elérhetetlen heap objektumokat szabadít fel (G1, ZGC, Shenandoah, ParallelGC…) |
A modern JVM-ek tiered compilation-t használnak: a kód interpreterrel indul (Tier 0), majd C1-en (1–3. szint) át végül C2-be kerül (4. szint), ahogy egyre "forróbbá" válik.
3. Gyakorlati használat
Mikor konfiguráljuk a JVM memóriabeállításokat?
- Produkciós telepítések — mindig állítsuk az
-Xms-t (kezdeti heap) egyenlővé az-Xmx-szel (max heap), hogy elkerüljük a heap-átméretezési szüneteket. - Konténerizált környezetek — használjuk a
-XX:+UseContainerSupportopciót (Java 10 óta alapból aktív), hogy a JVM a konténer memóriakorlátait olvassa a host RAM helyett. - Sok dinamikusan betöltött osztály (frameworkök, OSGi, app szerverek) — állítsuk be a
-XX:MaxMetaspaceSize-t a natív memória elszabadulásának megelőzésére. - Sok thread-et használó alkalmazások — csökkentsük az
-Xss-t (stack méret) a RAM-ba férhető thread-ek számának növeléséhez; mély rekurziónál növeljük.
Mikor NE hangoljuk túl?
- Fejlesztői/tesztkörnyezet — az alapbeállítások megfelelők; a korai hangolás időpazarlás.
- Véleményes runtime-ok (Spring Boot native, GraalVM native-image) — a JVM flagek nem alkalmazhatók; saját konfigurációjukat kell használni.
Főbb JVM flagek referencia
| Flag | Cél | Példa |
|---|---|---|
-Xms |
Kezdeti heap méret | -Xms512m |
-Xmx |
Maximális heap méret | -Xmx2g |
-Xss |
Thread stack méret | -Xss256k |
-XX:MaxMetaspaceSize |
Metaspace korlát | -XX:MaxMetaspaceSize=256m |
-XX:+PrintGCDetails |
Részletes GC naplózás | — |
-XX:+UseG1GC |
G1 collector kiválasztása | — |
-XX:+HeapDumpOnOutOfMemoryError |
Heap dump OOM esetén | — |
4. Kód példák
Alappélda — ClassLoader-ek vizsgálata
public class ClassLoaderInspector {
public static void main(String[] args) {
// Application classloader
ClassLoader appCL = ClassLoaderInspector.class.getClassLoader();
System.out.println("App CL: " + appCL);
// Platform (extension) classloader
ClassLoader platformCL = appCL.getParent();
System.out.println("Platform CL: " + platformCL);
// Bootstrap classloader — Java-ban null-ként jelenik meg
ClassLoader bootstrapCL = platformCL.getParent();
System.out.println("Bootstrap CL: " + bootstrapCL); // null
// Bootstrap által betöltött alap osztály
ClassLoader stringCL = String.class.getClassLoader();
System.out.println("String's CL: " + stringCL); // null (bootstrap)
}
}
Várt kimenet (Java 17+):
App CL: jdk.internal.loader.ClassLoaders$AppClassLoader@...
Platform CL: jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
Bootstrap CL: null
String's CL: null
Haladó példa — Stack Frame és StackOverflowError
public class StackDemo {
// Minden hívás egy új frame-et nyom a thread Java Stack-jére
static int countDown(int n) {
if (n == 0) return 0;
return 1 + countDown(n - 1); // rekurzív hívás = új stack frame
}
// Végtelen rekurzió → StackOverflowError
static void infinite() {
infinite();
}
public static void main(String[] args) {
System.out.println(countDown(10)); // 10-et ír ki, minden rendben
try {
infinite();
} catch (StackOverflowError e) {
// A StackOverflowError Error, nem Exception
System.out.println("Stack overflow elkapva: " + e);
}
}
}
Megjegyzés: A
StackOverflowErrorazError-ból, nem azException-ból örököl.catchblokkban helyreállítható, de a thread stackje kiszámíthatatlan állapotban van — a blokk után ne használjuk tovább érdemi munkára azt a thread-et.
Metaspace beállítások példa
# Futtatás korlátozott Metaspace-szel a natív memória elszabadulásának megakadályozásához
java -XX:MaxMetaspaceSize=128m \
-XX:MetaspaceSize=64m \
-XX:+PrintGCDetails \
-jar myapp.jar
Metaspace diagnosztika:
# Natív memóriahasználat részletes kiírása (szükséges: -XX:NativeMemoryTracking=summary)
java -XX:NativeMemoryTracking=summary -jar myapp.jar &
jcmd <PID> VM.native_memory summary
Gyakori buktató — OutOfMemoryError forgatókönyvek
import java.util.ArrayList;
import java.util.List;
public class OOMDemo {
// 1. Heap OOM — megtartjuk az objektumokat, hogy a GC ne tudja összegyűjteni
static void heapOOM() {
List<byte[]> leak = new ArrayList<>();
while (true) {
leak.add(new byte[1024 * 1024]); // 1 MB-os darabok
}
// java.lang.OutOfMemoryError: Java heap space kivételt dob
}
// 2. Metaspace OOM — futásidőben generált osztályok (pl. proxy/ASM)
// throws java.lang.OutOfMemoryError: Metaspace
// 3. Stack OOM (StackOverflowError)
static void stackOOM() {
stackOOM(); // határtalan rekurzió
// java.lang.StackOverflowError kivételt dob
}
}
5. Trade-offok
| Szempont | Részletek |
|---|---|
| ⚡ Teljesítmény | Nagyobb heap → kevesebb GC szünet, de hosszabb szünet amikor lefut; kisebb heap → gyakoribb, de rövidebb szünetek |
| 💾 Memória | A Metaspace natív memóriát használ a heap-en kívül; a MaxMetaspaceSize-t explicit be kell állítani, különben kimerülhet az OS memória |
| 🔧 Karbantarthatóság | A túlhangolt JVM flagek törékennyé teszik a konfigurációt JVM verziók és telepítési környezetek között |
| 🔄 Rugalmasság | A JIT tiered compilation futásidőben alkalmazkodik; a warm-up időt (~ezernyi meghívás) figyelembe kell venni benchmarkoknál |
| 🚀 Indulás | A JIT warm-up késleltetést okoz — ez problémás serverless/rövid életű folyamatoknál; a GraalVM native-image AOT alternatívát kínál |
| 🧵 Párhuzamosság | A heap megosztott → az objektum-allokáció thread-biztos a TLAB révén; a stackek thread-enkéntiek → nincs megosztás, nincs szinkronizáció |
6. Gyakori hibák
1. ❌ Az `-Xms` és `-Xmx` értékek nem egyeznek
# Rossz — a JVM kicsivel indul és időt pazarol heap-átméretezésre
java -Xmx4g -jar app.jar
# Jó — előre foglalja a teljes heap-et
java -Xms4g -Xmx4g -jar app.jar
2. ❌ Metaspace korlát figyelmen kívül hagyása
# Rossz — a Metaspace addig nő, amíg az OS kifogy a memóriából
java -jar app.jar
# Jó — korlátozza a Metaspace-t
java -XX:MaxMetaspaceSize=256m -jar app.jar
3. ❌ StackOverflowError elkapása és folytatás
// Rossz — a stack állapota sérült az overflow után
void process() {
try {
recursiveOp();
} catch (StackOverflowError e) {
process(); // újabb kód hívása egy esetleg sérült stacken
}
}
// Jó — naplózás, kecses leállás, nincs további rekurzió
void process() {
try {
recursiveOp();
} catch (StackOverflowError e) {
log.error("Stack overflow a process()-ben", e);
throw new RuntimeException("Feldolgozás sikertelen: rekurzió túl mély", e);
}
}
4. ❌ Heap és stack tárolás összekeverése
// A stack tárolja: primitív lokális változókat, objektum referenciákat
// A heap tárolja: a tényleges objektumpéldányokat
void example() {
int x = 42; // x a STACK-en van
String s = "hello"; // az 's' referencia a STACK-en; a String objektum a HEAP-en
Object obj = new Object(); // ugyanígy — referencia a stacken, objektum a heap-en
}
5. ❌ Az osztálybetöltés azonnaliságának feltételezése
// Az osztályok lustán töltődnek be — csak az első hivatkozáskor
// Ez azt jelenti, hogy a statikus inicializálók az első használatkor futnak, nem indításkor
class Config {
static {
System.out.println("Config betöltve"); // csak a Config első használatakor jelenik meg
}
}
7. Senior-szintű meglátások
Custom ClassLoader-ek
A custom classloader-ek a plugin rendszerek, hot-reload, OSGi és alkalmazásszerver izoláció alapjai. A Tomcat minden webalkalmazása saját WebAppClassLoader-ben fut, így a különböző appok osztályai nem ütköznek.
public class PluginClassLoader extends URLClassLoader {
public PluginClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// Child-first: előbb próbálja magát betölteni, csak utána delegál a szülőhöz
// Plugin izoláció szempontjából hasznos
synchronized (getClassLoadingLock(name)) {
Class<?> loaded = findLoadedClass(name);
if (loaded != null) return loaded;
try {
return findClass(name); // először saját classpath-on próbál
} catch (ClassNotFoundException e) {
return super.loadClass(name, resolve); // visszaesés a szülőre
}
}
}
}
Escape Analysis és stack allokáció
A JIT compiler escape analysis-t végez: ha egy objektum nem "szökik meg" a metódusból (nincs külső hivatkozás rá), a JVM allokálhatja a stack-en a heap helyett — ezzel teljesen elkerüli a GC terhelést.
// A Point objektum stack-en allokálható a C2 által, ha az escape analysis
// megállapítja, hogy nem hagyja el a metódust
double distanceFromOrigin(double x, double y) {
Point p = new Point(x, y); // esetleg soha nem kerül a heap-re
return Math.sqrt(p.x * p.x + p.y * p.y);
}
TLAB (Thread Local Allocation Buffer)
Hogy elkerülje a szinkronizációt minden new műveletnél, a JVM minden thread számára előre lefoglal egy privát Eden-darabot — a TLAB-ot. A TLAB-on belüli allokáció egyszerű pointer-léptetés — gyakorlatilag ingyenes.
TLAB működés röviden:
- Az Eden space egy részét a JVM threadenként külön TLAB-okra oszthatja.
- A thread a saját TLAB-jában egyszerű pointer bump technikával allokál.
- Ha a privát TLAB elfogy, új TLAB-ot kér vagy a megosztott Eden területre esik vissza.
Hangolható a -XX:TLABSize flag-gel, ha profiling sok TLAB-újratöltést mutat.
JVM hangolás konténerizált környezetekben
Java 10 előtt a JVM a host memóriát olvasta az ergonomikus alapbeállításokhoz, így jóval a konténer korlátain túlra méretezte a heap-et és a thread poolokat. Mindig ellenőrizzük:
java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|ActiveProcessor"
Hasznos diagnosztikai parancsok
# Összes JVM folyamat listázása
jps -l
# Heap hisztogram (legtöbb objektum típus darabszám/méret szerint)
jmap -histo <PID>
# Thread dump (holtpontok, blokkolt thread-ek felderítéséhez)
jstack <PID>
# JVM statisztikák valós időben
jstat -gcutil <PID> 1000 # GC statisztikák másodpercenként
8. Szószedet
| Kifejezés | Definíció |
|---|---|
| JVM | Java Virtual Machine — absztrakt gép, amely Java bytecode-ot hajt végre |
| Bytecode | Platformfüggetlen utasításkészlet, amelyet Java forrásból fordítanak; .class fájlokban tárolódik |
| ClassLoader | Osztálydefiníciókat futásidőben tölt be a JVM-be |
| Heap | Megosztott memóriaterület objektumpéldányok számára, amelyet a Garbage Collector kezel |
| Stack Frame | Metódusonkénti adatstruktúra a Java Stack-en; lokális változókat, operand stacket és visszatérési infót tartalmaz |
| Metaspace | Natív memóriaterület (Java 8 óta) osztály metaadatok tárolására; felváltotta a PermGen-t |
| PermGen | Permanent Generation — rögzített heap-terület osztály metaadatokhoz Java ≤ 7-ben; eltávolítva Java 8-ban |
| JIT | Just-In-Time fordító — hot bytecode utakat natív gépi kóddá fordít futásidőben |
| TLAB | Thread Local Allocation Buffer — thread-enkénti Eden-darab gyors, zármentes objektum-allokációhoz |
| Escape Analysis | JIT optimalizáció, amely meghatározza, hogy az objektumok allokálhatók-e stack-en vagy skaláris helyettesítéssel |
| Minor GC | Csak a Young Generation szemétgyűjtése |
| Major/Full GC | Az egész heap szemétgyűjtése (Young + Old Generation) |
9. Cheatsheet
- 🏗️ JVM = ClassLoader + Runtime Data Areas + Execution Engine
- 📦 ClassLoader-ek: Bootstrap → Platform → Application (parent-first delegálás)
- 🌐 Heap megosztott minden thread között; GC kezeli
- 🧵 Stack thread-enkénti; minden metódushívás = egy új stack frame
- 🗂️ Metaspace (Java 8+) osztály metaadatokat tárol natív memóriában — állítsuk be a
-XX:MaxMetaspaceSize-t - ⚙️ JIT hot kódot fordít natívvá; warm-up ezernyi meghívást igényel
- 🚨
StackOverflowError= stack frame-ek kimerültek (mély/végtelen rekurzió) - 🚨
OutOfMemoryError: Java heap space= heap tele, GC nem tud eleget felszabadítani - 🚨
OutOfMemoryError: Metaspace= túl sok betöltött osztály, korlátozzuk-XX:MaxMetaspaceSize-zal - ⚡ TLAB a
newműveletet gyakorlatilag ingyenessé teszi — pointer-léptetés egy thread-lokális Eden-darabban
🎮 Játékok
8 kérdés