La StackOverflowError en Java

Le StackOverflowError en Java

1. Vue d'ensemble

StackOverflowError peut être ennuyeux pour les développeurs Java, car c'est l'une des erreurs d'exécution les plus courantes que nous pouvons rencontrer.

Dans cet article, nous verrons comment cette erreur peut se produire en examinant divers exemples de code et comment nous pouvons y faire face.

2. Empiler les images et comment se produitStackOverflowError

Commençons par les bases. When a method is called, a new stack frame gets created on the call stack. Ce cadre de pile contient les paramètres de la méthode invoquée, ses variables locales et l'adresse de retour de la méthode, c'est-à-dire le point à partir duquel l'exécution de la méthode doit continuer après le retour de la méthode invoquée.

La création des cadres de pile se poursuivra jusqu'à la fin des invocations de méthodes trouvées dans les méthodes imbriquées.

Au cours de ce processus, si JVM rencontre une situation où il n'y a pas d'espace pour un nouveau cadre de pile à créer, il lancera unStackOverflowError.

La cause la plus courante de cette situation par la JVM estunterminated/infinite recursion - la description Javadoc deStackOverflowError mentionne que l'erreur est générée suite à une récursion trop profonde dans un extrait de code particulier.

Cependant, la récursivité n'est pas la seule cause de cette erreur. Cela peut également se produire dans une situation où une application conservecalling methods from within methods until the stack is exhausted. C'est un cas rare car aucun développeur ne suivrait intentionnellement de mauvaises pratiques de codage. Une autre cause rare esthaving a vast number of local variables inside a method.

LesStackOverflowError peuvent également être lancés lorsqu'une application est conçue pour avoircyclic relationships between classes. Dans cette situation, les constructeurs les uns des autres sont appelés de manière répétée, ce qui provoque la génération de cette erreur. Cela peut aussi être considéré comme une forme de récursion.

Un autre scénario intéressant qui provoque cette erreur est si unclass is being instantiated within the same class as an instance variable of that class. Cela entraînera l'appel du constructeur de la même classe encore et encore (récursivement), ce qui aboutira finalement à unStackOverflowError.

Dans la section suivante, nous examinerons quelques exemples de code illustrant ces scénarios.

3. StackOverflowError en action

Dans l'exemple ci-dessous, unStackOverflowError sera levé en raison d'une récursion involontaire, où le développeur a oublié de spécifier une condition d'arrêt pour le comportement récursif:

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

Ici, l'erreur est renvoyée à toutes les occasions pour toute valeur transmise à la méthode:

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);
    }
}

Cependant, dans l'exemple suivant, une condition de terminaison est spécifiée mais n'est jamais remplie si une valeur de-1 est passée à la méthodecalculateFactorial(), ce qui provoque une récursion sans fin / infinie:

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

Cet ensemble de tests illustre ce scénario:

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);
    }
}

Dans ce cas particulier, l'erreur aurait pu être complètement évitée si la condition de terminaison était simplement posée de la manière suivante:

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

Voici le test qui montre ce scénario en pratique:

public class RecursionWithCorrectTerminationConditionManualTest {
    @Test
    public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() {
        int numToCalcFactorial = -1;
        RecursionWithCorrectTerminationCondition rctc
          = new RecursionWithCorrectTerminationCondition();

        assertEquals(1, rctc.calculateFactorial(numToCalcFactorial));
    }
}

Examinons maintenant un scénario dans lequel lesStackOverflowError se produisent à la suite de relations cycliques entre les classes. ConsidéronsClassOne etClassTwo, qui s’instancient mutuellement à l’intérieur de leurs constructeurs, provoquant une relation cyclique:

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;
    }
}

Disons maintenant que nous essayons d'instancierClassOne comme vu dans ce test:

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

Cela se termine par unStackOverflowError puisque le constructeur deClassOne instancieClassTwo, et le constructeur deClassTwo instancie à nouveauClassOne. Et cela se produit à plusieurs reprises jusqu'à ce qu'il déborde la pile.

Ensuite, nous verrons ce qui se passe lorsqu'une classe est instanciée dans la même classe en tant que variable d'instance de cette classe.

Comme vu dans l'exemple suivant,AccountHolder s'instancie en tant que variable d'instancejointAccountHolder:

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

    AccountHolder jointAccountHolder = new AccountHolder();
}

Lorsque la classeAccountHolder est instanciée,, unStackOverflowError est lancé en raison de l'appel récursif du constructeur comme vu dans ce test:

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

4. Traiter avecStackOverflowError

La meilleure chose à faire quand unStackOverflowError est rencontré est d'inspecter la trace de pile avec précaution pour identifier le motif répétitif des numéros de ligne. Cela nous permettra de localiser le code présentant une récursion problématique.

Examinons quelques traces de pile causées par les exemples de code que nous avons vus précédemment.

Cette trace de pile est produite parInfiniteRecursionWithTerminationConditionManualTest si nous omettons la déclaration d'exception deexpected:

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)

Ici, on peut voir la ligne numéro 5 se répéter. C'est là que l'appel récursif est en cours. Il suffit maintenant d’examiner le code pour voir si la récursivité est effectuée correctement.

Voici la trace de pile que nous obtenons en exécutantCyclicDependancyManualTest (encore une fois, sans exceptionexpected):

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)

Cette trace de pile indique les numéros de ligne à l'origine du problème dans les deux classes en relation cyclique. Le numéro de ligne 9 deClassTwo et le numéro de ligne 9 deClassOne pointent vers l'emplacement à l'intérieur du constructeur où il essaie d'instancier l'autre classe.

Une fois que le code est minutieusement inspecté et si aucun des éléments suivants (ou toute autre erreur de logique de code) n'est à l'origine de l'erreur:

  • Récursion mal implémentée (c'est-à-dire sans condition de résiliation)

  • Dépendance cyclique entre les cours

  • Instanciation d'une classe dans la même classe en tant que variable d'instance de cette classe

Ce serait une bonne idée d'essayer d'augmenter la taille de la pile. En fonction de la machine virtuelle Java installée, la taille de pile par défaut peut varier.

L'indicateur-Xss peut être utilisé pour augmenter la taille de la pile, soit à partir de la configuration du projet ou de la ligne de commande.

5. Conclusion

Dans cet article, nous avons examiné de plus près lesStackOverflowError, y compris comment le code Java peut le provoquer et comment nous pouvons le diagnostiquer et le réparer.

Le code source lié à cet article peut être trouvéover on GitHub.