Középhaladó Olvasási idő: ~13 perc

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:

  1. A Java forráskód (.java) lefordul javac segítségével.
  2. Az eredmény a bytecode (.class).
  3. A platformon futó JVM ezt végrehajtja vagy tovább fordítja.
  4. 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:

  1. Az Application ClassLoader először a szülőjétől kéri a com.example.Foo betöltését.
  2. A Platform ClassLoader továbbadja a kérést felfelé.
  3. A Bootstrap ClassLoader próbálkozik először, és ha nem találja, visszaadja a vezérlést lefelé.
  4. 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:+UseContainerSupport opció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 StackOverflowError az Error-ból, nem az Exception-ból örököl. catch blokkban 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 new műveletet gyakorlatilag ingyenessé teszi — pointer-léptetés egy thread-lokális Eden-darabban

🎮 Játékok

8 kérdés