Maison  >  Article  >  Java  >  Comment résoudre le problème d'erreur StackOverflowError en Java

Comment résoudre le problème d'erreur StackOverflowError en Java

WBOY
WBOYavant
2023-05-13 20:49:043271parcourir

Introduction à StackOverflowError

StackOverflowError peut être ennuyeux pour les développeurs Java car il s'agit de l'une des erreurs d'exécution les plus courantes que nous pouvons rencontrer. Dans cet article, nous apprendrons comment cette erreur se produit en examinant divers exemples de code et comment y remédier. Comment se produisent Stack Frames et StackOverflowerError Commençons par les bases. Lorsqu'une méthode est appelée, un nouveau cadre de pile est créé sur la pile d'appels. Le cadre de pile contient les paramètres de la méthode appelée et ses variables locales.

StackOverflowError peut être ennuyeux pour les développeurs Java car il s'agit de l'une des erreurs d'exécution les plus courantes que nous pouvons rencontrer.

Dans cet article, nous comprendrons comment cette erreur se produit en examinant divers exemples de code et comment y remédier.

Comment Stack Frames et StackOverflowerError se produisent

Commençons par les bases. Lorsqu'une méthode est appelée, un nouveau cadre de pile est créé sur la pile d'appels. Ce cadre de pile contient les paramètres de la méthode appelée, ses variables locales et l'adresse de retour de la méthode, qui est le point auquel l'exécution de la méthode doit continuer après le retour de la méthode appelée.

La création de cadres de pile se poursuivra jusqu'à ce que la fin de l'appel de méthode dans la méthode imbriquée soit atteinte.

Au cours de ce processus, si la JVM rencontre une situation où il n'y a pas d'espace pour créer un nouveau cadre de pile, elle générera une erreur StackOverflower. StackOverflower 错误。

JVM遇到这种情况的最常见原因是未终止/无限递归——StackOverflowerr的Javadoc描述提到,错误是由于特定代码段中的递归太深而引发的。

然而,递归并不是导致此错误的唯一原因。在应用程序不断从方法内调用方法直到堆栈耗尽的情况下,也可能发生这种情况。这是一种罕见的情况,因为没有开发人员会故意遵循糟糕的编码实践。另一个罕见的原因是方法中有大量局部变量。

当应用程序设计为类之间具有循环关系时,也可以抛出StackOverflowError。在这种情况下,会重复调用彼此的构造函数,从而引发此错误。这也可以被视为递归的一种形式。

另一个引起此错误的有趣场景是,如果一个类在同一个类中作为该类的实例变量实例化。这将导致一次又一次(递归)调用同一类的构造函数,最终导致堆栈溢出错误。

StackOverflowerError正在运行

在下面所示的示例中,由于意外递归,开发人员忘记为递归行为指定终止条件,将抛出StackOverflowError错误:

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

在这里,对于传递到方法中的任何值,在任何情况下都会引发错误:

public class UnintendedInfiniteRecursionManualTest {
    @Test(expected = <a href="https://javakk.com/tag/stackoverflowerror" rel="external nofollow"  rel="external nofollow"      title="查看更多关于 StackOverflowError 的文章" target="_blank">StackOverflowError</a>.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);
    }
}

但是,在下一个示例中,指定了终止条件,但如果将值 -1 传递给 calculateFactorial() 方法,则永远不会满足终止条件,这会导致未终止/无限递归:

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

这组测试演示了此场景:

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

在这种特殊情况下,如果将终止条件简单地表示为:

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

下面的测试在实践中显示了这种情况:

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

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

现在让我们来看一个场景,其中StackOverflowError错误是由于类之间的循环关系而发生的。让我们考虑 ClassOne 和 ClassTwo ,它们在其构造函数中相互实例化,从而产生循环关系:

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

现在让我们假设我们尝试实例化ClassOne,如本测试中所示:

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

这最终导致了StackOverflowError错误,因为 ClassOne 的构造函数实例化了 ClassTwo ,而 ClassTwo 的构造函数再次实例化了 ClassOne 。这种情况反复发生,直到它溢出堆栈。

接下来,我们将看看当一个类作为该类的实例变量在同一个类中实例化时会发生什么。

如下一个示例所示, AccountHolder 将自身实例化为实例变量 JointaCountHolder :

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

当 AccountHolder 类实例化时,由于构造函数的递归调用,会引发StackOverflowError错误,如本测试中所示:

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

解决StackOverflowError

当遇到StackOverflowError堆栈溢出错误时,最好的做法是仔细检查堆栈跟踪,以识别行号的重复模式。这将使我们能够定位具有问题递归的代码。

让我们研究一下由我们前面看到的代码示例引起的几个堆栈跟踪。

如果忽略预期的异常声明,则此堆栈跟踪由 InfiniteCursionWithTerminationConditionManualTest 生成:

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)

在这里,可以看到第5行重复。这就是进行递归调用的地方。现在只需要检查代码,看看递归是否以正确的方式完成。

下面是我们通过执行 CyclicDependancyManualTest

La raison la plus courante pour laquelle JVM rencontre ce problème est la récursion interminable/infinie - La description Javadoc de StackOverflowerr mentionne que l'erreur est causée par une récursivité trop profonde dans un morceau de code spécifique. 🎜🎜 Cependant, la récursivité n'est pas la seule cause de cette erreur. Cela peut également se produire dans des situations où une application continue d'appeler des méthodes depuis une méthode jusqu'à ce que la pile soit épuisée. Il s'agit d'une situation rare, car aucun développeur ne suivrait intentionnellement de mauvaises pratiques de codage. Une autre raison rare est le grand nombre de variables locales dans la méthode. 🎜🎜StackOverflowError peut également être généré lorsque l'application est conçue pour avoir des relations cycliques entre les classes. Dans ce cas, les constructeurs des autres sont appelés à plusieurs reprises, provoquant cette erreur. Cela peut également être considéré comme une forme de récursion. 🎜🎜Un autre scénario intéressant qui provoque cette erreur est si une classe est instanciée dans la même classe en tant que variable d'instance de cette classe. Cela entraînera l'appel du constructeur de la même classe encore et encore (de manière récursive), conduisant finalement à une erreur de débordement de pile. 🎜🎜StackOverflowError en action🎜🎜Dans l'exemple ci-dessous, en raison d'une récursion inattendue, le développeur a oublié de spécifier une condition de fin pour le comportement récursif, une erreur StackOverflowError sera générée : 🎜
java.lang.StackOverflowError
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)
🎜Ici, pour toute valeur passée dans la méthode , Dans tous les cas, une erreur est générée : 🎜rrreee🎜 Cependant, dans l'exemple suivant, la condition de terminaison est spécifiée, mais si la valeur -1 est passée au calculateFacttorial() code>, alors la condition de terminaison n'est jamais remplie, ce qui entraîne une récursion interminable/infinie : 🎜rrreee🎜 Cet ensemble de tests démontre ce scénario : 🎜rrreee🎜Dans ce cas particulier, si la condition de terminaison est simplement exprimée comme : 🎜rrreee 🎜 Le test ci-dessous montre cette situation en pratique : 🎜rrreee🎜 Examinons maintenant un scénario dans lequel une erreur StackOverflowError se produit en raison d'une relation circulaire entre les classes. Considérons <code>ClassOne et ClassTwo qui s'instancient mutuellement dans leurs constructeurs, créant ainsi une relation circulaire : 🎜rrreeerrreee🎜 Supposons maintenant que nous essayons d'instancier ClassOne, comme illustré dans ce test : 🎜rrreee🎜Cela conduit finalement à une StackOverflowError car le constructeur de ClassOne instancie ClassTwo tandis que le constructeur de ClassTwo instancie ClassOne à nouveau. Cela se produit à plusieurs reprises jusqu'à ce que la pile déborde. 🎜🎜Ensuite, nous verrons 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 JointaCountHolder : 🎜rrreee🎜Lorsque la classe AccountHolder est instanciée, puisque Appels récursifs au constructeur, générant une StackOverflowError, comme indiqué dans ce test : 🎜rrreee🎜Résolution de StackOverflowError🎜🎜Lorsque vous rencontrez une StackOverflowError, la meilleure pratique consiste à examiner attentivement la trace de la pile pour identifier les duplications du modèle de numéros de ligne. Cela nous permettra de localiser le code avec une récursion problématique. 🎜🎜Examinons quelques traces de pile causées par l'exemple de code que nous avons vu plus tôt. 🎜🎜Si la déclaration d'exception attendue est ignorée, cette trace de pile est générée par InfiniteCursionWithTerminationConditionManualTest : 🎜rrreee🎜Ici, vous pouvez voir que la ligne 5 est répétée. C'est là que les appels récursifs sont effectués. Il ne reste plus qu'à vérifier le code pour voir si la récursion est effectuée correctement. 🎜🎜Voici la trace de pile que nous obtenons en exécutant CyclicDependancyManualTest (encore une fois, aucune exception attendue) : 🎜
java.lang.StackOverflowError
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)
  at c.b.s.ClassTwo.<init>(ClassTwo.java:9)
  at c.b.s.ClassOne.<init>(ClassOne.java:9)

该堆栈跟踪显示了在循环关系中的两个类中导致问题的行号。ClassTwo的第9行和ClassOne的第9行指向构造函数中试图实例化另一个类的位置。

彻底检查代码后,如果以下任何一项(或任何其他代码逻辑错误)都不是错误的原因:

  • 错误实现的递归(即没有终止条件)

  • 类之间的循环依赖关系

  • 在同一个类中实例化一个类作为该类的实例变量

尝试增加堆栈大小是个好主意。根据安装的JVM,默认堆栈大小可能会有所不同。

-Xss 标志可以用于从项目的配置或命令行增加堆栈的大小。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer