W3docs

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.

AspektStackHeap
EnthältFrames: lokale Variablen, Primitive, ReferenzenObjekte, arrays, Instanzfelder
GeltungsbereichEiner pro ThreadEiner, geteilt von der gesamten JVM
LebensdauerFrame wird bei Methodenrückkehr entferntBis unerreichbar, dann GC
AllokationPush/Pop, extrem schnellnew, verwaltet durch den Allokator
GrößeBegrenzt (-Xss); Überlauf wirft StackOverflowErrorBegrenzt (-Xmx); Erschöpfung wirft OutOfMemoryError
BereinigungAutomatisch, deterministischGarbage 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 GC

Instanzfelder 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 characters

Wenn 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.

java— editable, runs on the server

Was man aus dem Durchlauf mitnehmen kann:

  • score bleibt 10, während das n der Methode 110 erreicht. Das Primitiv wurde in den neuen Frame kopiert, sodass nichts, was die Methode tat, zurück in main reichen konnte — das ist Pass-by-Value für Primitive, sichtbar gemacht.
  • a.value und b.value sind beide 42 und a == b ist true, weil Counter b = a eine 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) ist a.value 999. 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) ist a.value immer noch 999, nicht -1. Das Neuzuweisen des Parameters hat nur die lokale Kopie der Referenz der Methode umgeleitet; das a des Aufrufers wurde nie bewegt. Das Mutieren des Objekts funktioniert; das Neubinden der Variable nicht.
  • lit1 == lit2 ist true, aber lit1 == obj ist false, während .equals für beide true ist. Gepoolte Literale teilen sich ein Heap-Objekt; new String erzwingt ein separates. Der erreichbare, aber überlebbare StackOverflowError und das abschließende temp = null zeigen die Grenzen der beiden Bereiche und wie der Heap sammelbar wird, wenn seine letzte Referenz fällt.

Übungen

Übung
Eine Methode erhält einen Parameter vom Typ StringBuilder. Innerhalb der Methode rufen Sie sb.append('x') auf. Nach der Rückkehr der Methode zeigt das StringBuilder-Objekt des Aufrufers das angehängte 'x'. Warum?
Eine Methode erhält einen Parameter vom Typ StringBuilder. Innerhalb der Methode rufen Sie sb.append('x') auf. Nach der Rückkehr der Methode zeigt das StringBuilder-Objekt des Aufrufers das angehängte 'x'. Warum?

Verwandte Kapitel

Was this page helpful?