Der StackOverflowError in Java

1. Übersicht

StackOverflowError kann für Java-Entwickler ärgerlich sein, da es einer der häufigsten Laufzeitfehler ist, auf die wir stoßen können.

In diesem Artikel werden wir anhand verschiedener Codebeispiele sehen, wie dieser Fehler auftreten kann, und wie wir damit umgehen können.

2. Stack Frames und wie StackOverflowError auftritt

Beginnen wir mit den Grundlagen. Wenn eine Methode aufgerufen wird, wird ein neuer Stapelrahmen auf dem Aufrufstapel erstellt. Dieser Stapelrahmen enthält Parameter der aufgerufenen Methode, ihre lokalen Variablen und die Rücksprungadresse der Methode, dh den Punkt, ab dem die Methodenausführung fortgesetzt werden soll, nachdem die aufgerufene Methode zurückgegeben wurde.

Die Erstellung von Stapelrahmen wird fortgesetzt, bis das Ende der in verschachtelten Methoden gefundenen Methodenaufrufe erreicht ist.

Wenn JVM während dieses Vorgangs auf eine Situation stößt, in der kein Platz für die Erstellung eines neuen Stapelrahmens vorhanden ist, wird ein StackOverflowError ausgelöst .

Die häufigste Ursache für diese Situation in der JVM ist eine nicht abgeschlossene / unendliche Rekursion. In der Javadoc-Beschreibung für StackOverflowError wird erwähnt, dass der Fehler aufgrund einer zu tiefen Rekursion in einem bestimmten Code-Snippet ausgelöst wird.

Die Rekursion ist jedoch nicht die einzige Ursache für diesen Fehler. Dies kann auch vorkommen, wenn eine Anwendung Methoden innerhalb von Methoden aufruft, bis der Stapel erschöpft ist . Dies ist ein seltener Fall, da kein Entwickler absichtlich schlechte Codierungspraktiken befolgen würde. Eine weitere seltene Ursache ist eine große Anzahl lokaler Variablen in einer Methode .

Die Stackoverflow kann auch ausgelöst werden , wenn eine Anwendung ausgelegt ist , hat c yclic Beziehungen zwischen den Klassen . In dieser Situation werden die Konstruktoren voneinander wiederholt aufgerufen, wodurch dieser Fehler ausgelöst wird. Dies kann auch als eine Form der Rekursion betrachtet werden.

Ein weiteres interessantes Szenario, das diesen Fehler verursacht, besteht darin, dass eine Klasse innerhalb derselben Klasse wie eine Instanzvariable dieser Klasse instanziiert wird . Dies führt dazu, dass der Konstruktor derselben Klasse immer wieder (rekursiv) aufgerufen wird, was schließlich zu einem StackOverflowError führt.

Im nächsten Abschnitt werden einige Codebeispiele vorgestellt, die diese Szenarien veranschaulichen.

3. StackOverflowError in Aktion

In dem unten gezeigten Beispiel wird ein StackOverflowError aufgrund einer unbeabsichtigten Rekursion ausgelöst, bei der der Entwickler vergessen hat, eine Beendigungsbedingung für das rekursive Verhalten anzugeben:

public class UnintendedInfiniteRecursion { public int calculateFactorial(int number) { return number * calculateFactorial(number - 1); } }

Hier wird der Fehler bei allen Gelegenheiten ausgelöst, die an die Methode übergeben werden:

public class UnintendedInfiniteRecursionManualTest { @Test(expected = StackOverflowError.class) public void givenPositiveIntNoOne_whenCalFact_thenThrowsException() { int numToCalcFactorial= 1; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } @Test(expected = StackOverflowError.class) public void givenPositiveIntGtOne_whenCalcFact_thenThrowsException() { int numToCalcFactorial= 2; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } @Test(expected = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException() { int numToCalcFactorial= -1; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } }

Im nächsten Beispiel wird jedoch eine Beendigungsbedingung angegeben, die jedoch niemals erfüllt wird, wenn ein Wert von -1 an die berechneFactorial () -Methode übergeben wird, was zu einer nicht abgeschlossenen / unendlichen Rekursion führt:

public class InfiniteRecursionWithTerminationCondition { public int calculateFactorial(int number) { return number == 1 ? 1 : number * calculateFactorial(number - 1); } }

Diese Testreihe demonstriert dieses Szenario:

public class InfiniteRecursionWithTerminationConditionManualTest { @Test public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); assertEquals(1, irtc.calculateFactorial(numToCalcFactorial)); } @Test public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); assertEquals(120, irtc.calculateFactorial(numToCalcFactorial)); } @Test(expected = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException() { int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); irtc.calculateFactorial(numToCalcFactorial); } }

In diesem speziellen Fall hätte der Fehler vollständig vermieden werden können, wenn die Beendigungsbedingung einfach wie folgt formuliert worden wäre:

public class RecursionWithCorrectTerminationCondition { public int calculateFactorial(int number) { return number <= 1 ? 1 : number * calculateFactorial(number - 1); } }

Hier ist der Test, der dieses Szenario in der Praxis zeigt:

public class RecursionWithCorrectTerminationConditionManualTest { @Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = new RecursionWithCorrectTerminationCondition(); assertEquals(1, rctc.calculateFactorial(numToCalcFactorial)); } }

Betrachten wir nun ein Szenario, in dem der StackOverflowError als Ergebnis zyklischer Beziehungen zwischen Klassen auftritt . Betrachten wir ClassOne und ClassTwo , die sich in ihren Konstruktoren gegenseitig instanziieren und eine zyklische Beziehung verursachen:

public class ClassOne { private int oneValue; private ClassTwo clsTwoInstance = null; public ClassOne() { oneValue = 0; clsTwoInstance = new ClassTwo(); } public ClassOne(int oneValue, ClassTwo clsTwoInstance) { this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; } }
public class ClassTwo { private int twoValue; private ClassOne clsOneInstance = null; public ClassTwo() { twoValue = 10; clsOneInstance = new ClassOne(); } public ClassTwo(int twoValue, ClassOne clsOneInstance) { this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; } }

Nehmen wir nun an, wir versuchen, ClassOne wie in diesem Test gezeigt zu instanziieren :

public class CyclicDependancyManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException() { ClassOne obj = new ClassOne(); } }

Diese endet mit einem bis Stackoverflow da der Konstruktor von ClassOne ist Instanziieren ClassTwo, und der Konstruktor von ClassTwo wieder Instanziieren ClassOne. Und dies geschieht wiederholt, bis der Stapel überläuft.

Als nächstes schauen wir uns an, was passiert, wenn eine Klasse innerhalb derselben Klasse wie eine Instanzvariable dieser Klasse instanziiert wird.

Wie im nächsten Beispiel zu sehen ist, instanziiert sich AccountHolder als Instanzvariable JointAccountHolder :

public class AccountHolder { private String firstName; private String lastName; AccountHolder jointAccountHolder = new AccountHolder(); }

Wenn die AccountHolder- Klasse instanziiert wird , wird aufgrund des rekursiven Aufrufs des Konstruktors, wie in diesem Test gezeigt , ein StackOverflowError ausgelöst:

public class AccountHolderManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() { AccountHolder holder = new AccountHolder(); } }

4. Umgang mit StackOverflowError

Wenn ein StackOverflowError auftritt , überprüfen Sie den Stack-Trace am besten vorsichtig, um das sich wiederholende Muster der Zeilennummern zu ermitteln. Auf diese Weise können wir den Code finden, der eine problematische Rekursion aufweist.

Lassen Sie uns einige Stapelspuren untersuchen, die durch die Codebeispiele verursacht wurden, die wir zuvor gesehen haben.

Diese Stapelverfolgung wird von InfiniteRecursionWithTerminationConditionManualTest erstellt, wenn die erwartete Ausnahmedeklaration weggelassen wird:

java.lang.StackOverflowError at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5) at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5) at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5) at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)

Hier ist zu sehen, wie sich Zeile 5 wiederholt. Hier wird der rekursive Aufruf ausgeführt. Jetzt müssen Sie nur noch den Code untersuchen, um festzustellen, ob die Rekursion korrekt durchgeführt wurde.

Hier ist die Stapelverfolgung, die wir durch Ausführen von CyclicDependancyManualTest erhalten (erneut ohne erwartete Ausnahme):

java.lang.StackOverflowError at c.b.s.ClassTwo.(ClassTwo.java:9) at c.b.s.ClassOne.(ClassOne.java:9) at c.b.s.ClassTwo.(ClassTwo.java:9) at c.b.s.ClassOne.(ClassOne.java:9)

Diese Stapelverfolgung zeigt die Zeilennummern an, die das Problem in den beiden Klassen verursachen, die in einer zyklischen Beziehung stehen. Die Zeilennummer 9 von ClassTwo und die Zeilennummer 9 von ClassOne zeigen auf die Position innerhalb des Konstruktors, an der versucht wird, die andere Klasse zu instanziieren.

Sobald der Code gründlich überprüft wurde und keiner der folgenden Fehler (oder ein anderer Codelogikfehler) die Fehlerursache ist:

  • Falsch implementierte Rekursion (dh ohne Beendigungsbedingung)
  • Zyklische Abhängigkeit zwischen Klassen
  • Instanziieren einer Klasse innerhalb derselben Klasse wie eine Instanzvariable dieser Klasse

Es wäre eine gute Idee, die Stapelgröße zu erhöhen. Abhängig von der installierten JVM kann die Standardstapelgröße variieren.

Das Flag -Xss kann verwendet werden, um die Größe des Stapels entweder über die Projektkonfiguration oder über die Befehlszeile zu erhöhen.

5. Schlussfolgerung

In diesem Artikel haben wir uns den StackOverflowError genauer angesehen, einschließlich der Frage, wie Java-Code ihn verursachen kann und wie wir ihn diagnostizieren und beheben können.

Der Quellcode zu diesem Artikel ist auf GitHub zu finden.