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 une variété d'exemples de code ainsi que la façon dont nous pouvons y faire face.

2. Stack Frames et comment StackOverflowError se produit

Commençons par les bases. Lorsqu'une méthode est appelée, un nouveau cadre de pile est créé sur la pile d'appels. Cette trame 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 se poursuivre après le retour de la méthode invoquée.

La création des cadres de pile se poursuivra jusqu'à ce qu'elle atteigne la fin des appels de méthode trouvés dans les méthodes imbriquées.

Au cours de ce processus, si JVM rencontre une situation dans laquelle il n'y a pas d'espace pour un nouveau cadre de pile à créer, il lèvera une StackOverflowError .

La cause la plus courante pour la JVM de rencontrer cette situation est la récursivité sans fin / infinie - la description Javadoc de StackOverflowError mentionne que l'erreur est générée à la suite d'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 continue d' appeler des méthodes à partir de méthodes jusqu'à ce que la pile soit épuisée . C'est un cas rare car aucun développeur ne suivrait intentionnellement de mauvaises pratiques de codage. Une autre cause rare est d' avoir un grand nombre de variables locales dans une méthode .

Le StackOverflowError peut également être levé lorsqu'une application est conçue pour avoir des relations c ycliques entre les classes . Dans cette situation, les constructeurs les uns des autres sont appelés de manière répétitive, ce qui provoque l'émission de cette erreur. Cela peut également être considéré comme une forme de récursivité.

Un autre scénario intéressant qui provoque cette erreur est si une classe est instanciée dans la même classe qu'une variable d'instance de cette classe . Cela entraînera l'appel du constructeur de la même classe encore et encore (récursivement), ce qui aboutira finalement à une StackOverflowError.

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

3. StackOverflowError en action

Dans l'exemple ci-dessous, une StackOverflowError sera levée en raison d'une récursivité involontaire, où le développeur a oublié de spécifier une condition de terminaison 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 transmise à la méthode CalculateFactorial () , ce qui provoque une récursivité 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 comme suit:

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 StackOverflowError se produit à la suite de relations cycliques entre les classes. Considérons ClassOne et ClassTwo , 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; } }

Maintenant, disons que nous essayons d'instancier ClassOne comme vu dans ce test:

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

Cela se termine par une StackOverflowError puisque le constructeur de ClassOne instancie ClassTwo et que le constructeur de ClassTwo instancie à nouveau ClassOne. Et cela se produit à plusieurs reprises jusqu'à ce qu'il déborde de la pile.

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

Comme le montre l'exemple suivant, AccountHolder s'instancie en tant que variable d'instance jointAccountHolder :

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

Lorsque la classe AccountHolder est instanciée , une StackOverflowError est levée 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. Traitement de StackOverflowError

La meilleure chose à faire lorsqu'une StackOverflowError est rencontrée est d'inspecter la trace de la pile avec précaution pour identifier le motif répétitif des numéros de ligne. Cela nous permettra de localiser le code qui a une récursivité 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 par InfiniteRecursionWithTerminationConditionManualTest si nous omettons la déclaration d'exception attendue :

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 effectué. Il ne reste plus qu'à examiner le code pour voir si la récursivité est effectuée correctement.

Voici la trace de la pile que nous obtenons en exécutant CyclicDependancyManualTest (encore une fois, sans exception attendue ):

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 affiche les numéros de ligne à l'origine du problème dans les deux classes qui sont dans une relation cyclique. La ligne numéro 9 de ClassTwo et la ligne numéro 9 de ClassOne pointent vers l'emplacement à l'intérieur du constructeur où il tente d'instancier l'autre classe.

Une fois le code soigneusement inspecté et si aucune des situations suivantes (ou toute autre erreur de logique de code) n'est à l'origine de l'erreur:

  • Récursivité mal implémentée (c'est-à-dire sans condition de fin)
  • Dépendance cyclique entre les classes
  • 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. Selon 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 le StackOverflowError, 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 est disponible à l'adresse over sur GitHub.