Java Stack vs. Heap-Speicher
Wie sich Java-Stack und Heap unterscheiden, was sich in jedem befindet und der Lebenszyklus von Variablen und Objekten.
Zur Laufzeit teilt die JVM den verwalteten Speicher in zwei Bereiche mit sehr unterschiedlichen Aufgaben auf. Der Stack enthält die Verwaltungsdaten für Methodenaufrufe — einen Frame pro Aufruf, mit den lokalen Variablen und primitiven Werten der Methode. Der Heap enthält jedes Objekt, das Sie mit new erstellen, wird vom gesamten Programm gemeinsam genutzt und vom Garbage Collector freigegeben. Fast jedes verwirrende Java-Verhalten — warum eine Methode Ihren int „nicht ändern kann", warum zwei Variablen dieselbe Bearbeitung „sehen", warum tiefe Rekursion zum Absturz führt — resultiert direkt aus dieser Aufteilung.
Dieses Kapitel behandelt, was in jedem Bereich gespeichert wird, wie sich ihre Lebenszyklen unterscheiden, warum Javas Pass-by-Value-Regel direkt aus der Aufteilung folgt, den Sonderfall des String-Pools und was passiert, wenn einer der Bereiche erschöpft ist. Für das größere Bild, wie diese Bereiche in die Laufzeit passen, siehe JVM-Architektur und das Java Memory Model.
Zwei Bereiche, zwei Lebenszyklen
Der Stack ist thread-spezifisch und automatisch: Wenn eine Methode aufgerufen wird, legt die JVM einen Frame an, und wenn die Methode zurückkehrt, wird dieser Frame entfernt und seine lokalen Variablen verschwinden sofort. Der Heap ist gemeinsam genutzt und verwaltet: Objekte leben, bis keine Referenz mehr auf sie zeigt, wonach der Garbage Collector den Speicher freigeben kann. Nichts auf dem Heap verschwindet in dem Moment, in dem eine Methode zurückkehrt.
| Aspekt | Stack | Heap |
|---|---|---|
| Enthält | Frames: lokale Variablen, Primitive, Referenzen | Objekte, arrays, Instanzfelder |
| Geltungsbereich | Einer pro Thread | Einer, geteilt von der gesamten JVM |
| Lebensdauer | Frame wird bei Methodenrückkehr entfernt | Bis unerreichbar, dann GC |
| Allokation | Push/Pop, extrem schnell | new, verwaltet durch den Allokator |
| Größe | Begrenzt (-Xss); Überlauf wirft StackOverflowError | Begrenzt (-Xmx); Erschöpfung wirft OutOfMemoryError |
| Bereinigung | Automatisch, deterministisch | Garbage Collector, nicht-deterministisch |
Was tatsächlich wo gespeichert ist
Eine lokale Variable lebt immer im aktuellen Stack-Frame. Was sie enthält, hängt von ihrem Typ ab. Bei einem Primitiv enthält der Frame den Wert selbst. Bei einem Objekttyp enthält der Frame nur eine Referenz — das Objekt, auf das sie zeigt, lebt auf dem Heap.
void example() {
int count = 5; // the value 5 sits in the frame (stack)
double rate = 0.5; // likewise on the stack
int[] data = new int[3]; // 'data' (a reference) is on the stack,
// the 3-element array is on the heap
Point p = new Point(1, 2); // 'p' is on the stack, the Point is on the heap
} // frame popped: count, rate, data, p all gone;
// the array and Point survive until GCInstanzfelder sind Teil des Objekts und leben daher zusammen mit ihm auf dem Heap — auch ein Feld eines primitiven Typs. Ein private int balance innerhalb eines Account-Objekts ist Heap-Speicher, kein Stack-Speicher, weil es zum Objekt gehört, nicht zu einem einzelnen Methodenaufruf.
Java ist immer Pass-by-Value
Java kopiert das Argument bei jedem Aufruf in den Parameter. Bei einem Primitiv kopiert es den Wert; bei einem Objekt kopiert es die Referenz. Es gibt kein Pass-by-Reference in Java, und diese eine Regel (weiter untersucht in Methodenparameter) erklärt die drei klassischen Überraschungen:
static void bumpPrimitive(int n) { n++; } // changes the copy only
static void mutate(StringBuilder sb) { sb.append("!"); } // edits shared object
static void rebind(StringBuilder sb) { // points the copy elsewhere
sb = new StringBuilder("new"); // caller's variable unchanged
}bumpPrimitive kann den Aufrufer nicht beeinflussen: Es empfing eine Kopie der Zahl. mutate kann das ändern, was der Aufrufer sieht, weil die kopierte Referenz immer noch auf das Objekt des Aufrufers auf dem Heap zeigt. rebind kann das nicht, weil das Neuzuweisen des Parameters nur die lokale Kopie der Referenz umleitet, nicht die Variable des Aufrufers.
Der String-Pool, ein Heap-Sonderfall
String-Literale werden interniert: identische Literale teilen sich ein Objekt im String-Pool, sodass == (Referenzidentität) für sie true zurückgibt. Das Schreiben von new String("hi") erzwingt ein separates Heap-Objekt, sodass == false zurückgibt, obwohl die Zeichen übereinstimmen. Deshalb vergleichen Sie Strings mit .equals(), das den Inhalt prüft, nicht die Identität.
String a = "hi";
String b = "hi";
String c = new String("hi");
a == b; // true — both point at the pooled literal
a == c; // false — c is a distinct heap object
a.equals(c); // true — same charactersWenn die Bereiche erschöpft sind
Jeder Bereich hat eine Grenze und seinen eigenen Fehlermodus. Unbegrenzte Rekursion legt immer mehr Frames an, bis der Stack erschöpft ist und StackOverflowError ausgelöst wird. Objekte schneller zu allozieren, als der GC sie freigeben kann, erschöpft den Heap und löst OutOfMemoryError aus. Beide sind Errors, keine Exceptions — Zeichen eines strukturellen Problems (ein fehlender Basisfall, ein Leak) und nicht etwas, das routinemäßig abgefangen werden sollte.
static int countDown(int n) {
return countDown(n - 1); // no base case -> StackOverflowError
}Ein Arbeitsbeispiel: Die zwei Bereiche in Aktion
Dieses Programm berührt in einem Durchlauf alle oben genannten Regeln: ein Primitiv, das per Wert übergeben wird, zwei Referenzen auf ein Heap-Objekt, eine Mutation, die der Aufrufer sieht, eine Neuzuweisung, die er nicht sieht, den String-Pool, einen absichtlichen Stack-Overflow und das Fallen-Lassen der letzten Referenz auf ein Objekt.
Was man aus dem Durchlauf mitnehmen kann:
scorebleibt10, während dasnder Methode110erreicht. Das Primitiv wurde in den neuen Frame kopiert, sodass nichts, was die Methode tat, zurück inmainreichen konnte — das ist Pass-by-Value für Primitive, sichtbar gemacht.a.valueundb.valuesind beide42unda == bisttrue, weilCounter b = aeine Referenz kopiert hat, nicht das Objekt. Eine Heap-Instanz, zwei Stack-Variablen, die darauf zeigen — über eine davon bearbeiten und beide „sehen" es.- Nach
mutateThroughReference(a)ista.value999. Die Methode erhielt eine Kopie der Referenz, aber die Kopie zeigte noch auf dasselbe Heap-Objekt, sodass die Feldänderung für den Aufrufer sichtbar ist. - Nach
reassignReference(a)ista.valueimmer noch999, nicht-1. Das Neuzuweisen des Parameters hat nur die lokale Kopie der Referenz der Methode umgeleitet; dasades Aufrufers wurde nie bewegt. Das Mutieren des Objekts funktioniert; das Neubinden der Variable nicht. lit1 == lit2isttrue, aberlit1 == objistfalse, während.equalsfür beidetrueist. Gepoolte Literale teilen sich ein Heap-Objekt;new Stringerzwingt ein separates. Der erreichbare, aber überlebbareStackOverflowErrorund das abschließendetemp = nullzeigen die Grenzen der beiden Bereiche und wie der Heap sammelbar wird, wenn seine letzte Referenz fällt.
Übungen
Verwandte Kapitel
- JVM-Architektur — wo Stack und Heap innerhalb der Laufzeit liegen.
- Java Memory Model — wie sich Speicher über Threads hinweg verhält.
- Garbage Collection — wie unerreichbare Heap-Objekte freigegeben werden.
- Methodenparameter — Pass-by-Value in der Tiefe.
- Referenzen — wie Variablen auf Heap-Objekte zeigen.
- Der String-Pool — warum identische Literale ein Objekt teilen.