Der StackOverflowError in Java

Der StackOverflowError in Java

1. Überblick

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

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

2. Stapelrahmen und wieStackOverflowError auftritt

Beginnen wir mit den Grundlagen. When a method is called, a new stack frame gets created on the call stack. Dieser Stapelrahmen enthält Parameter der aufgerufenen Methode, ihrer lokalen Variablen und der Rücksprungadresse der Methode, d.h. der Punkt, an dem die Methodenausführung fortgesetzt werden soll, nachdem die aufgerufene Methode zurückgegeben wurde.

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

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 einStackOverflowError ausgelöst.

Die häufigste Ursache dafür, dass die JVM auf diese Situation stößt, istunterminated/infinite recursion. In der Javadoc-Beschreibung fürStackOverflowError wird erwähnt, dass der Fehler aufgrund einer zu tiefen Rekursion in einem bestimmten Codefragment ausgelöst wird.

Rekursion ist jedoch nicht die einzige Ursache für diesen Fehler. Dies kann auch in einer Situation passieren, in der eine Anwendungcalling methods from within methods until the stack is exhausted behält. Dies ist ein seltener Fall, da kein Entwickler absichtlich schlechte Codierungspraktiken befolgt. Eine weitere seltene Ursache isthaving a vast number of local variables inside a method.

DieStackOverflowError können auch ausgelöst werden, wenn eine Anwendungcyclic relationships between classes enthält. In dieser Situation werden die Konstruktoren der anderen immer wieder aufgerufen, wodurch dieser Fehler ausgelöst wird. Dies kann auch als eine Form der Rekursion angesehen werden.

Ein weiteres interessantes Szenario, das diesen Fehler verursacht, ist, wenn aclass is being instantiated within the same class as an instance variable of that class. Dies führt dazu, dass der Konstruktor derselben Klasse immer wieder (rekursiv) aufgerufen wird, was schließlich zu einemStackOverflowError. führt

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

3. StackOverflowError in Aktion

In dem unten gezeigten Beispiel wird einStackOverflowError aufgrund einer unbeabsichtigten Rekursion ausgelöst, wobei 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);
    }
}

In diesem Fall wird der Fehler bei jedem Wert ausgelöst, der an die Methode übergeben wird:

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 MethodecalculateFactorial() ü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 Tests demonstrieren 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 ausgedrückt 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 demStackOverflowError als Ergebnis zyklischer Beziehungen zwischen Klassen auftritt. Betrachten wirClassOne undClassTwo, die sich innerhalb ihrer 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 zu instanziieren, wie in diesem Test gezeigt:

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

Dies endet mit einemStackOverflowError, da der Konstruktor vonClassOneClassTwo, instanziiert und der Konstruktor vonClassTwo erneutClassOne. instanziiert. Dies geschieht wiederholt, bis er überläuft der Stapel.

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 sichAccountHolder als InstanzvariablejointAccountHolder:

public class AccountHolder {
    private String firstName;
    private String lastName;

    AccountHolder jointAccountHolder = new AccountHolder();
}

Wenn die KlasseAccountHolder instanziiert wird, wird, aStackOverflowError aufgrund des rekursiven Aufrufs des Konstruktors ausgelöst, wie in diesem Test gezeigt:

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

4. Umgang mitStackOverflowError

Wenn einStackOverflowError angetroffen wird, ist es am besten, die Stapelverfolgung vorsichtig zu untersuchen, um das sich wiederholende Muster der Zeilennummern zu identifizieren. Auf diese Weise können wir den Code finden, bei dem es zu einer problematischen Rekursion kommt.

Lassen Sie uns einige Stapelspuren untersuchen, die durch die zuvor gezeigten Codebeispiele verursacht wurden.

Diese Stapelverfolgung wird vonInfiniteRecursionWithTerminationConditionManualTest erzeugt, wenn die Ausnahmedeklaration vonexpectedweggelassen 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 der Stack-Trace, den wir durch Ausführen vonCyclicDependancyManualTest erhalten (wiederum ohneexpected 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, die das Problem in den beiden Klassen verursachen, die sich in einer zyklischen Beziehung befinden. Die Zeilennummer 9 vonClassTwo und die Zeilennummer 9 vonClassOne 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 Fehler in der Codelogik) die Fehlerursache ist:

  • Falsch implementierte Rekursion (d. H. ohne Kündigungsbedingung)

  • 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-Xss-Flag kann verwendet werden, um die Größe des Stapels entweder über die Projektkonfiguration oder über die Befehlszeile zu erhöhen.

5. Fazit

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

Der Quellcode zu diesem Artikel istover on GitHub.