A StackOverflowError Java-ban

1. Áttekintés

StackOverflowError bosszantó lehet a Java fejlesztők számára, mivel ez az egyik leggyakoribb futásidejű hiba, amellyel találkozhatunk.

Ebben a cikkben megnézzük, hogyan fordulhat elő ez a hiba, megnézve a különféle kód példákat, valamint hogy hogyan kezelhetjük azt.

2. Veremkeretek és hogyan StackOverflowError Bekövetkezik

Kezdjük az alapokkal. Egy módszer meghívásakor új veremkeret jön létre a hívásveremben. Ez a veremkeret tartalmazza a meghívott módszer paramétereit, helyi változóit és a módszer visszatérési címét, vagyis azt a pontot, ahonnan a metódus végrehajtásának folytatódnia kell a meghívott módszer visszatérése után.

A veremkeretek létrehozása addig folytatódik, amíg el nem éri a beágyazott módszerek belsejében található metódus-invokációk végét.

E folyamat során, ha a JVM olyan helyzettel találkozik, amikor nincs hely egy új veremkeret létrehozására, akkor egy StackOverflowError.

A JVM leggyakoribb oka, hogy találkozik ezzel a helyzettel befejezetlen / végtelen rekurzió - a Javadoc leírása StackOverflowError megemlíti, hogy a hibát egy adott kódrészlet túl mély rekurziója okozza.

A rekurzió azonban nem az egyetlen oka ennek a hibának. Ez történhet olyan helyzetben is, amikor egy alkalmazás megtartja metódusok meghívása a metódusokon belülről, amíg a verem kimerül. Ez ritka eset, mivel egyetlen fejlesztő sem követné szándékosan a rossz kódolási gyakorlatokat. Egy másik ritka ok az amelynek rengeteg helyi változója van a módszer belsejében.

A StackOverflowError akkor is dobható, ha egy alkalmazást arra terveztek cosztályok közötti yclic kapcsolatok. Ebben a helyzetben egymás konstruktorait ismétlődő módon hívják, ami ezt a hibát eldobja. Ez a rekurzió egyik formájának is tekinthető.

Egy másik érdekes forgatókönyv, amely ezt a hibát okozza, ha a osztály ugyanazon osztályon belül példányosítva van, mint az adott osztály példányváltozója. Ez azt eredményezi, hogy ugyanazon osztály konstruktorát újra és újra (rekurzívan) hívják meg, ami végül a StackOverflowError.

A következő szakaszban néhány kód példát nézünk meg, amelyek bemutatják ezeket a forgatókönyveket.

3. StackOverflowError akcióban

Az alábbi példában a StackOverflowError nem szándékos rekurzió miatt dobódik, ahol a fejlesztő elfelejtette megadni a rekurzív viselkedés megszüntetési feltételét:

public class UnintendedInfiniteRecursion {public int calcFactorial (int szám) {return number * calcFactorial (szám - 1); }}

Itt a hibát minden alkalommal eldobják a módszerbe átadott értékek:

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

A következő példában azonban megadunk egy felmondási feltételt, de soha nem teljesül, ha értéke -1 átadják a calcFactorial () módszer, amely megszakítatlan / végtelen rekurziót okoz:

public class InfiniteRecursionWithTerminationCondition {public int calcFactorial (int szám) {visszatérési szám == 1? 1: szám * calcFactorial (szám - 1); }}

Ez a tesztkészlet a következő esetet mutatja be:

public class InfiniteRecursionWithTerminationConditionManualTest {@Test public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = új InfiniteRecursionWithTerminationCondition (); assertEquals (1, irtc.calculateFactorial (numToCalcFactorial)); } @Test public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = új InfiniteRecursionWithTerminationCondition (); assertEquals (120, irtc.calculateFactorial (numToCalcFactorial)); } @Test (várható = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = új InfiniteRecursionWithTerminationCondition (); irtc.calculateFactorial (numToCalcFactorial); }}

Ebben a konkrét esetben a hiba teljesen elkerülhető lett volna, ha a felmondási feltétel egyszerűen a következő:

public class RecursionWithCorrectTerminationCondition {public int calcFactorial (int szám) {return szám <= 1? 1: szám * calcFactorial (szám - 1); }}

Az alábbi teszt bemutatja ezt a forgatókönyvet a gyakorlatban:

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

Most nézzünk meg egy olyan forgatókönyvet, ahol a StackOverflowError osztályok közötti ciklikus kapcsolatok eredményeként történik. Vegyük fontolóra ClassOne és ClassTwo, amelyek ciklikus kapcsolatot okoznak a konstruktőreikben:

public class ClassOne {private int oneValue; privát ClassTwo clsTwoInstance = null; public ClassOne () {oneValue = 0; clsTwoInstance = új ClassTwo (); } public ClassOne (int oneValue, ClassTwo clsTwoInstance) {this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; }}
public class ClassTwo {private int twoValue; privát ClassOne clsOneInstance = null; public ClassTwo () {twoValue = 10; clsOneInstance = új ClassOne (); } public ClassTwo (int twoValue, ClassOne clsOneInstance) {this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; }}

Tegyük fel, hogy megpróbálunk példányosítani ClassOne amint ez a teszt látható:

public class CyclicDependancyManualTest {@Test (várható = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException () {ClassOne obj = new ClassOne (); }}

Ennek a végén a StackOverflowError mivel a kivitelező ClassOne példányos ClassTwo, és a kivitelezője ClassTwo ismét instantizál ClassOne. És ez többször megtörténik, amíg túlcsordul a verem.

Ezután megvizsgáljuk, mi történik, ha egy osztályt ugyanazon osztályon belül példányosítunk, mint az adott osztály példányváltozóját.

Amint a következő példában látható, Számlatulajdonos példányváltozóként példázza magát jointAccountHolder:

public class AccountHolder {private String keresztnév; privát karakterlánc vezetéknév; AccountHolder jointAccountHolder = új AccountHolder (); }

Amikor az Számlatulajdonos osztály példányos, a StackOverflowError a konstruktor rekurzív hívása miatt dobódik el, amint ez a teszt látható:

public class AccountHolderManualTest {@Test (várható = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException () {AccountHolder holder = new AccountHolder (); }}

4. Foglalkozás StackOverflowError

A legjobb dolog, ha a StackOverflowError előfordul, hogy óvatosan ellenőrizze a verem nyomát, hogy azonosítsa a sorszámok ismétlődő mintázatát. Ez lehetővé teszi számunkra a problémás rekurzióval rendelkező kód megkeresését.

Vizsgáljuk meg néhány veremnyomot, amelyeket a korábban látott kódpéldák okoztak.

Ezt a verem nyomot a InfiniteRecursionWithTerminationConditionManualTest ha kihagyjuk a várt kivétel nyilatkozat:

java.lang.StackOverflowError at cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) at cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) at cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) at cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java : 5)

Itt az 5. sor ismétlődhet. Itt történik a rekurzív hívás. Most csak a kód megvizsgálásának kérdése, hogy a rekurzió helyesen történt-e.

Itt van a verem nyom, amelyet végrehajtással kapunk CyclicDependancyManualTest (megint nélkül várt kivétel):

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. Class.ne.ja.ClassOne.

Ez a veremkövetés megmutatja a problémát okozó sorszámokat a két ciklikus kapcsolatban álló osztályban. Sor 9. sora ClassTwo és a. sor 9. sora ClassOne mutasson a konstruktoron belüli helyre, ahol megpróbálja példányosítani a másik osztályt.

Miután a kódot alaposan megvizsgálták, és ha az alábbiak egyike (vagy bármely más kódlogikai hiba) nem okozza a hibát:

  • Helytelenül végrehajtott rekurzió (azaz befejezési feltétel nélkül)
  • Az osztályok közötti ciklikus függőség
  • Osztályozás egy osztályon belül ugyanazon osztályon belül, mint az adott osztály példányváltozója

Jó lenne megpróbálni növelni a verem méretét. A telepített JVM-től függően az alapértelmezett vereméret változhat.

A -Xss A flag használatával növelhető a verem mérete, akár a projekt konfigurációjából, akár a parancssorból.

5. Következtetés

Ebben a cikkben közelebbről megvizsgáltuk a StackOverflowError beleértve azt is, hogy a Java kód hogyan okozhatja, és hogyan tudjuk diagnosztizálni és kijavítani.

A cikkhez kapcsolódó forráskód megtalálható a GitHub oldalon.