Du hast sicher schon erlebt, dass eine Variable „weg“ ist, sobald eine Methode fertig ist. Gleichzeitig scheinen Objekte manchmal ewig zu leben - bis plötzlich der Speicher knapp wird. Das ist kein Zufall, sondern hängt daran, wo Java Dinge ablegt. Wenn du Heap, Stack und Metaspace einmal sauber auseinanderhalten kannst, werden viele typische Anfänger-Fragezeichen (und einige nervige Fehler) deutlich kleiner.
Lebensdauer statt „Magie“
In Java ist eine Variable erst einmal nur ein Name, also ein Verweis oder ein Wert. Entscheidend ist die Lebensdauer dieses Namens: Lokale Variablen existieren nur so lange, wie der aktuelle Methodenaufruf aktiv ist. Das ist der Stack-Teil der Geschichte. Objekte dagegen liegen im Heap und leben so lange, wie es noch einen Weg zu ihnen gibt (also eine Referenz, die sie erreichbar macht). Und die Metaspace-Ecke ergänzt das Bild: Klassen selbst brauchen auch Speicher - nicht als Objekte im Heap, sondern als Metadaten.
Wenn du dir merkst:
- Stack = Aufrufkontext
- Heap = Objekte und
- Metaspace = Klasseninfos hast du schon das wichtigste Gerüst.
Stack: der kurze Atem von lokalen Variablen
Der Stack ist pro Thread organisiert. Jeder Methodenaufruf legt ein neues Stack-Frame an. In diesem Frame landen unter anderem Parameter und lokale Variablen. Sobald die Methode zurückkehrt, wird dieses Frame entfernt. Damit verschwinden die lokalen Variablennamen automatisch - ohne Garbage Collector, einfach weil der Aufrufkontext nicht mehr existiert.
Das ist der Grund, warum sich dieser Code so „vergesslich“ verhält:
static void demo() {
String s = "Hallo";
Person p = new Person("Marcy");
// ...
} // hier endet das Stack-Frame: s und p sind als Variablen weg
Wichtig: Hier verschwindet nicht sofort das Objekt Person, sondern nur die lokale Variable p, die darauf gezeigt hat. Ob das Objekt danach noch lebt, hängt davon ab, ob es noch irgendwo anders referenziert wird.
Typische Stack-Probleme sind eher logisch als speicherlastig: Wenn du eine Methode rekursiv ohne Abbruchbedingung aufrufst, stapelst du Frames, bis der Stack voll ist. Dann siehst du StackOverflowError. Das ist kein „zu kleiner Heap“, sondern ein zu tiefer Aufruf-Stack.
Heap: Objekte leben, solange sie erreichbar sind
Der Heap ist der gemeinsame Speicherbereich für Objekte. „Gemeinsam“ heißt: Alle Threads können auf Objekte zugreifen (über Referenzen), aber die Lebensdauer hängt nicht an einem einzelnen Methodenaufruf. Ein Objekt im Heap wird erst dann interessant für den Garbage Collector, wenn es nicht mehr erreichbar ist.
Das kannst du dir mit einem kleinen „Escape“-Beispiel klar machen:
static Person cached;
static void demo() {
Person p = new Person("Alex");
cached = p; // p „entkommt“ in ein Feld mit längerer Lebensdauer
} // p als Variable ist weg, aber cached hält das Objekt am Leben
Nach dem Methodenende ist p weg, aber das Objekt lebt weiter, weil cached noch darauf zeigt. Genau so entstehen viele Speicherprobleme in echten Anwendungen: Nicht, weil Java „nicht aufräumt“, sondern weil irgendwo noch Referenzen hängen. Häufige Kandidaten sind static Felder, Caches ohne Begrenzung oder Listener-Listen, aus denen nie entfernt wird.
Wenn der Heap wirklich voll läuft, bekommst du typischerweise OutOfMemoryError: Java heap space. Das heißt nicht automatisch, dass du „mehr RAM kaufen“ musst. Oft heißt es: Du hältst mehr Objekte fest, als du denkst, oder du erzeugst in Schleifen unnötig viele Objekte.
Ein wichtiger Punkt für das Gefühl: Der Garbage Collector arbeitet nicht nach dem Motto „am Ende der Methode alles löschen“, sondern nach Erreichbarkeit. Das kann bedeuten, dass ein Objekt logisch nicht mehr gebraucht wird, aber technisch noch referenziert ist. Dann bleibt es.
Metaspace: Klassen sind auch Daten
Neben Variablen und Objekten gibt es noch etwas, das Speicher braucht: Klasseninformationen. Welche Felder hat die Klasse? Welche Methoden? Welche Annotationen? Welche Bytecode-Strukturen? Diese Metadaten liegen seit Java 8 nicht mehr in der alten PermGen, sondern in der Metaspace.
Der entscheidende Unterschied: Metaspace liegt im nativen Speicher (also außerhalb des Java-Heaps) und wächst standardmäßig nach Bedarf, bis eine Grenze erreicht ist oder das Betriebssystem nicht mehr mitspielt. Wenn hier etwas schief läuft, sieht die Fehlermeldung anders aus, zum Beispiel OutOfMemoryError: Metaspace.
In Server-Anwendungen (z.B. wenn Anwendungen neu geladen werden) spielt zusätzlich der ClassLoader eine Rolle: Klassen hängen immer an einem ClassLoader. Erst wenn ein ClassLoader inklusive aller zugehörigen Klassen nicht mehr referenziert ist, können diese Klassen-Metadaten wieder freigegeben werden (Class Unloading). Wenn du ClassLoader irgendwo festhältst oder ständig neue Klassen dynamisch nachlädst (Frameworks, Proxies), kann Metaspace wachsen, obwohl dein Heap noch „gesund“ aussieht.
Für dein Grundverständnis reicht: Heap-Probleme sind oft „zu viele Objekte“ oder „Referenzen hängen“, Metaspace-Probleme sind oft „zu viele Klassen“ oder „ClassLoader werden nicht frei“.
Drei typische Irrtümer, die dir Zeit sparen
Viele Anfänger stolpern über die gleichen Denkfehler.
Erstens: „Variable weg = Objekt weg“. Nein. Variablen sind Namen im Stack (oder Felder in Objekten). Objekte liegen im Heap. Die Variable kann verschwinden, während das Objekt weiterlebt - oder umgekehrt kann eine Variable noch existieren, aber null sein, weil sie auf nichts zeigt.
Zweitens: „Garbage Collector räumt sofort auf“. Nein. Er räumt, wenn er es für nötig hält und wenn es sich lohnt. Du kannst die JVM nicht sinnvoll dazu zwingen, jetzt sofort aufzuräumen. System.gc() ist höchstens ein Hinweis und in vielen Umgebungen praktisch wirkungslos oder sogar unerwünscht.
Drittens: „OutOfMemoryError bedeutet immer Heap“. Nein. Lies die genaue Fehlermeldung. „Java heap space“ ist Heap, „Metaspace“ ist Metaspace. Und bei „unable to create new native thread“ kann es um Thread-Stacks und Betriebssystem-Limits gehen.
Praxis: wie du das im Alltag greifbar machst
Wenn du verstehen willst, warum ein Objekt nicht verschwindet, such nach der Referenzkette. In IntelliJ hilft dir der Debugger: Schau dir Variablen an, prüfe Felder, und achte besonders auf static. In laufenden Anwendungen liefern Heap-Dumps und Tools wie VisualVM oder Java Mission Control die „Wer hält wen fest?“-Antwort.
Für einen schnellen Reality-Check sind JVM-Optionen hilfreich. Die Namen musst du nicht auswendig können, aber du solltest wissen, dass es sie gibt:
-Xmsund-Xmxsetzen Start- und Max-Heap.-Xsssetzt die Stack-Größe pro Thread.-XX:MaxMetaspaceSizebegrenzt Metaspace, wenn du ein Wachstum deckeln willst.
Wenn du an einer Anwendung arbeitest, die auf WildFly läuft, gilt das genauso: Heap ist Heap, Stack ist Stack, Metaspace ist Metaspace. Der Container ändert nicht die Grundregeln, er macht nur manche Fehler sichtbarer, weil dort mehr Klassen geladen werden und mehr Threads aktiv sind.
Fazit: „Verschwinden“ ist meistens nur ein Kontextwechsel
Der Stack ist der Kurzzeitspeicher deiner Methodenaufrufe: Variablen verschwinden, weil der Kontext endet. Der Heap ist der Langzeitspeicher für Objekte: Sie bleiben, solange sie erreichbar sind. Und Metaspace hält die Baupläne: Klassenmetadaten, die an ClassLoader gebunden sind.
Wenn du beim Debuggen oder beim Lesen einer Exception nicht nur auf den Code schaust, sondern auch auf diese drei Speicherbereiche, wirst du schneller erkennen, ob du gerade ein Lebensdauer-Problem (Referenzen) oder ein Mengen-Problem (zu viele Objekte/Klassen) hast. Und genau das ist der Punkt, an dem „Java macht irgendwas“ zu „okay, ich weiß, wo ich suchen muss“ wird.
