Blog

Leckender Speicher in Java

Jeroen van Erp

Jeroen van Erp

Aktualisiert Oktober 23, 2025
3 Minuten

Erinnern wir uns nicht alle an die Zeit, als wir noch in C oder C++ programmiert haben? Sie mussten new und delete verwenden, um Objekte explizit zu erstellen und zu entfernen. Manchmal mussten Sie sogar malloc() verwenden, um eine bestimmte Menge an Speicher zu reservieren. Bei all diesen Konstrukten mussten Sie besonders darauf achten, dass Sie hinterher aufräumen, da sonst Speicherplatz verloren ging.

Heute jedoch, in den Tagen von Java, machen sich die meisten Leute nicht mehr so viele Gedanken über Speicherlecks. Die gängige Meinung ist, dass der Java Garbage Collector die Aufräumarbeiten hinter Ihnen erledigen wird. Das ist natürlich in allen normalen Fällen völlig richtig. Aber manchmal kann der Garbage Collector nicht aufräumen, weil Sie immer noch eine Referenz haben, auch wenn Sie das nicht wussten. Ich bin beim Lesen von JavaPedia über dieses kleine Programm gestolpert, das deutlich zeigt, dass auch Java zu unbeabsichtigten Speicherlecks fähig ist.

public class TestGC {
  private String large = new String(new char[100000]);
  public String getSubString() {
  return this.large.substring(0,2);
  }
  public static void main(String[] args) {
  ArrayList subStrings = new ArrayList();
  for (int i = 0; i  <  1000000; i++) {
  TestGC testGC = new TestGC();
  subStrings.add(testGC.getSubString());
  }
  }
}

Wenn Sie das Programm nun ausführen, werden Sie feststellen, dass es mit einem Stacktrace wie dem folgenden abstürzt: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.lang.String.(String.java:174) at TestGC.(TestGC.java:4) at TestGC.main(TestGC.java:13) Warum passiert das? Wir sollten nur 1.000.000 Strings der Länge 2 speichern, richtig? Das wären etwa 40 MB, die problemlos in den PermGen-Speicherplatz passen sollten. Was ist hier also passiert? Werfen wir einen Blick auf die substring-Methode in der String-Klasse.

public class String {
  // Paket privater Konstruktor, der das Wertefeld für die Geschwindigkeit teilt.
  String(int offset, int count, char value[]) {
  this.value = Wert;
  this.offset = offset;
  this.count = count;
  }
  public String substring(int beginIndex, int endIndex) {
  if (beginIndex count) {
  throw new StringIndexOutOfBoundsException(endIndex);
  }
  wenn (beginIndex  >  endIndex) {
  throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
  }
  return ((beginIndex == 0) && (endIndex == count)) ? this : 
  new String(offset + beginIndex, endIndex - beginIndex, value);
  }

Wir sehen, dass der Aufruf von substring einen neuen String mit dem gegebenen geschützten Konstruktor des Pakets erzeugt. Und der einzeilige Kommentar zeigt sofort, wo das Problem liegt. Das Zeichenarray wird gemeinsam mit der großen Zeichenfolge verwendet. Anstatt also sehr kleine Teilstrings zu speichern, haben wir jedes Mal den großen String gespeichert, aber mit einem anderen Offset und einer anderen Länge. Dieses Problem erstreckt sich auch auf andere Operationen, wie String.split() und <em?java.util.regex.Matcher.group(). Das Problem lässt sich leicht vermeiden, indem Sie das Programm wie folgt anpassen:

public class TestGC {
  private String large = new String(new char[100000]);
  public String getSubString() {
  return new String(this.large.substring(0,2)); //  <-- behebt das Leck!
  }
  public static void main(String[] args) {
  ArrayList subStrings = new ArrayList();
  for (int i = 0; i  <  1000000; i++) {
  TestGC testGC = new TestGC();
  subStrings.add(testGC.getSubString());
  }
  }
}

Ich habe schon oft gehört und auch die Meinung geteilt, dass der String-Kopierkonstruktor nutzlos ist und Probleme mit nicht-internen Strings verursacht. Aber in diesem Fall scheint er eine Daseinsberechtigung zu haben, da er das Zeichenarray effektiv beschneidet und uns davon abhält, eine Referenz auf den sehr großen String zu behalten.

Verfasst von

Jeroen van Erp

Contact

Let’s discuss how we can support your journey.