Kellerstapel und Halde
Auf der vorigen Seite stand ein Satz, über den man leicht hinwegliest: Bei einem Objekttyp steht in der Variablen „ein Verweis auf das Objekt im Speicher“.
Welcher Speicher? Und wo steht dann die Variable selbst?
Java teilt den Arbeitsspeicher eines laufenden Programms in zwei Bereiche auf, die sich völlig unterschiedlich verhalten. Wer sie einmal auseinanderhält, kann von da an drei Fragen beantworten, die vorher Glückssache waren: warum lokale Variablen nach dem Methodenaufruf weg sind, warum Objekte es nicht sind, und warum ein fehlender Basisfall bei Rekursion ausgerechnet einen Stapelüberlauf auslöst.
Die beiden Bereiche
- Der Kellerstapel (englisch call stack, auch Aufrufstapel) verwaltet die Methodenaufrufe. Für jeden laufenden Aufruf liegt dort genau ein Kellerrahmen (englisch stack frame). Er enthält die Parameter und die lokalen Variablen dieses einen Aufrufs sowie die Stelle, an die nach dem
returnzurückgesprungen wird. - Die Halde (englisch heap) verwaltet die Objekte. Alles, was mit
newentsteht – auch jedes Feld –, liegt dort.
Eine Variable vom Objekttyp liegt also im Kellerrahmen; das Objekt, auf das sie zeigt, liegt auf der Halde. Der Pfeil zwischen beiden ist die Referenz.
Der Name „Keller“ ist kein Zufall: Es ist derselbe Datentyp, den du als Stapel noch selbst implementieren wirst. Wer zuletzt kam, geht zuerst – ein Aufruf kann nur zurückkehren, wenn alle Aufrufe, die er selbst gestartet hat, schon zurückgekehrt sind.
Das Beispiel
Sage ohne Rechner voraus, was ausgegeben wird. Achte dabei besonders auf die Variable neuer in erzeuge: Sie existiert nach dem return nicht mehr. Was ist dann mit dem Punkt?
Auflösung
p: (13, 14)
Der Punkt entsteht in erzeuge, überlebt das Ende dieser Methode und wird danach von verschiebeUm verändert. Drei Methodenaufrufe waren beteiligt, alle drei sind längst beendet – das Objekt gibt es trotzdem noch.
Genau das ist der Unterschied zwischen den beiden Bereichen: Der Kellerrahmen ist an den Aufruf gebunden, das Objekt nicht.
Setze die Schritte im Diagramm um.
Warum die lokale Variable verschwindet und das Objekt nicht
Beim Aufruf einer Methode legt Java einen Kellerrahmen auf den Stapel, bei return nimmt es ihn wieder herunter. Alles, was im Rahmen stand, ist damit weg – Parameter wie lokale Variablen.
Weg ist damit aber nur die Variable, nicht das Objekt. Das liegt auf der Halde und wird nicht mit abgeräumt. Solange irgendeine Referenz darauf zeigt, bleibt es.
Deshalb kann eine Methode ein Objekt zurückgeben, das sie selbst angelegt hat – eine lokale Zahl könnte sie ebenfalls zurückgeben, aber nur als Kopie ihres Wertes.
a) In erzeuge steht Punkt neuer = new Punkt(pX, pY);. Benenne genau, welcher Teil dieser Zeile im Kellerrahmen landet und welcher auf der Halde.
b) verschiebeUm gibt nichts zurück und verändert den Punkt trotzdem sichtbar. Erkläre das mit den beiden Speicherbereichen.
c) Warum wäre es ein Fehler, wenn Java beim return auch das Objekt abräumen würde, auf das eine lokale Variable zeigt?
Auflösung
a) Im Kellerrahmen von erzeuge landet die Variable neuer – sie ist vier oder acht Byte groß und enthält nur die Referenz. Auf der Halde landet das, was new Punkt(...) erzeugt: ein Objekt mit den beiden Attributen x und y.
b) pPunkt ist eine Kopie der Referenz, kein zweites Objekt. Sie liegt in einem eigenen Kellerrahmen, zeigt aber auf dasselbe Objekt auf der Halde. verschiebe ändert die Attribute dort – und dorthin zeigt auch p in main.
c) Dann könnte keine Methode je ein Objekt zurückgeben. erzeuge würde eine Referenz auf etwas liefern, das im selben Moment gelöscht wurde. Objekte müssen ihren Erzeuger überleben können, sonst ließen sich keine Datenstrukturen bauen – eine Liste entsteht schließlich Knoten für Knoten in Methodenaufrufen, die alle längst beendet sind.
Was mit Objekten passiert, die niemand mehr braucht
Die Halde wächst also, während der Kellerstapel auf und ab geht. Läuft sie irgendwann über?
Nein – dafür sorgt die Speicherbereinigung (englisch garbage collection). Java erkennt selbstständig, auf welche Objekte keine Referenz mehr zeigt, und gibt deren Platz wieder frei. Um wen sich das dreht, entscheidet allein die Erreichbarkeit: Ein Objekt, zu dem kein Pfeil mehr führt, kann das Programm auch nie wieder benutzen.
In Sprachen wie C muss man jedes Objekt von Hand wieder freigeben. Vergisst man es, wächst der Verbrauch immer weiter – man nennt das ein Speicherleck.
void main() {
Punkt p = new Punkt(1, 1);
p = new Punkt(2, 2);
}
Nach der zweiten Zeile gibt es zwei Objekte auf der Halde, aber nur eine Variable. Erkläre, was mit dem ersten Punkt geschieht und warum das Programm ihn nicht wiederfinden kann – auch nicht, wenn es wollte.
Und der Stapelüberlauf?
Der Kellerstapel ist begrenzt. Jeder noch nicht zurückgekehrte Aufruf belegt einen Rahmen; passt kein weiterer mehr darauf, bricht das Programm mit einem Stapelüberlauf ab (StackOverflowError).
Im Normalfall merkt man davon nichts: Ein paar hundert verschachtelte Aufrufe sind kein Problem. Gefährlich wird es erst, wenn eine Methode sich selbst aufruft und nicht aufhört – dann wächst der Stapel, bis nichts mehr geht.
Diesem Fehler begegnest du im nächsten Kapitel wieder. Er ist dort kein Unglück, sondern die zuverlässigste Rückmeldung, die es gibt: Eine Rekursion ohne Basisfall meldet sich sofort, statt stumm hängenzubleiben.
Die beiden Bereiche im Überblick
| Kellerstapel | Halde | |
|---|---|---|
| Was liegt dort? | Kellerrahmen: Parameter, lokale Variablen, Rücksprungstelle | Objekte, also alles aus new – auch Felder |
| Wer legt an? | jeder Methodenaufruf | jedes new |
| Wann wird aufgeräumt? | beim return, sofort und automatisch |
wenn keine Referenz mehr zeigt, durch die Speicherbereinigung |
| Reihenfolge | streng: zuletzt angelegt, zuerst abgeräumt | beliebig |
| Größe | begrenzt, daher der Stapelüberlauf | wesentlich größer |
Diese Aufteilung ist keine Eigenheit von Java. Fast jede Programmiersprache trennt so – die Begriffe stack und heap begegnen dir überall wieder, ebenso wie der Stapelüberlauf.
Warum das wichtig wird
Das Modell trägt durch den ganzen Lernpfad:
- Bei der Rekursion erklärt es, warum jeder Aufruf seine eigenen Werte hat und wo die Grenze liegt.
- Beim Beurteilen von Algorithmen ist der zusätzliche Speicherbedarf eines rekursiven Verfahrens genau die Höhe des Kellerstapels.
- Bei den dynamischen Datenstrukturen liegen alle Knoten auf der Halde, verbunden durch Referenzen. Deshalb kann eine Liste wachsen, ohne dass vorher jemand ihre Größe kennt.