StackOverflowError v Jave

1. Prehľad

StackOverflowError môže byť pre vývojárov Java nepríjemný, pretože je to jedna z najbežnejších runtime chýb, s ktorými sa môžeme stretnúť.

V tomto článku uvidíme, ako môže k tejto chybe dôjsť, pri pohľade na rôzne príklady kódov a tiež o tom, ako s nimi môžeme nakladať.

2. Rámy stohu a ako StackOverflowError Vyskytuje

Začnime od základov. Keď sa zavolá metóda, v zásobníku hovorov sa vytvorí nový rámec zásobníka. Tento rámec zásobníka obsahuje parametre vyvolanej metódy, jej miestne premenné a spiatočnú adresu metódy, t. J. Bod, od ktorého by malo vykonávanie metódy pokračovať po návrate vyvolanej metódy.

Vytváranie rámcov zásobníka bude pokračovať, až kým sa nedosiahne koniec vyvolaných metód, ktoré sa nachádzajú vo vnorených metódach.

Ak počas tohto procesu dôjde k situácii, že JVM nebude mať priestor na vytvorenie nového rámca zásobníka, vrhne StackOverflowError.

Najbežnejšou príčinou, aby sa JVM stretol s touto situáciou, je neukončená / nekonečná rekurzia - popis Javadoc pre StackOverflowError uvádza, že chyba je vyvolaná v dôsledku príliš hlbokej rekurzie v konkrétnom útržku kódu.

Rekurzia však nie je jedinou príčinou tejto chyby. Môže sa to stať aj v situácii, keď sa aplikácia zachová volanie metód zvnútra metód až do vyčerpania zásobníka. Toto je ojedinelý prípad, pretože žiadny vývojár by zámerne nedodržiaval zlé postupy kódovania. Ďalšou zriedkavou príčinou je ktoré majú v metóde obrovské množstvo lokálnych premenných.

The StackOverflowError možno tiež vyhodiť, keď je aplikácia navrhnutá tak, aby mala cyklické vzťahy medzi triedami. V tejto situácii sa konštruktéri navzájom volajú opakovane, čo spôsobí vyhodenie tejto chyby. Toto sa dá považovať aj za formu rekurzie.

Ďalším zaujímavým scenárom, ktorý spôsobuje túto chybu, je, ak a trieda je inštancovaná v rámci tej istej triedy ako inštančná premenná tejto triedy. To spôsobí, že konštruktor rovnakej triedy bude volaný znova a znova (rekurzívne), čo nakoniec vyústi do a StackOverflowError.

V nasledujúcej časti sa pozrieme na niektoré príklady kódu, ktoré demonštrujú tieto scenáre.

3. StackOverflowError v akcii

V príklade zobrazenom nižšie a StackOverflowError bude vyhodené z dôvodu neúmyselnej rekurzie, keď vývojár zabudol určiť podmienku ukončenia rekurzívneho správania:

verejná trieda UnintendedInfiniteRecursion {public int countFactorial (int číslo) {návratové číslo * CalculateFactorial (číslo - 1); }}

Tu sa pri všetkých príležitostiach odovzdaných do metódy pri všetkých príležitostiach vyvolá chyba:

verejná trieda UnintendedInfiniteRecursionManualTest {@Test (očakávaný = StackOverflowError.class) public void givenPositiveIntNoOne_whenCalFact_thenThrowsException () {int numToCalcFactorial = 1; UnintendedInfiniteRecursion uir = nový UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (očakáva sa = StackOverflowError.class) public void givenPositiveIntGtOne_whenCalcFact_thenThrowsException () {int numToCalcFactorial = 2; UnintendedInfiniteRecursion uir = nový UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (očakáva sa = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; UnintendedInfiniteRecursion uir = nový UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); }}

V nasledujúcom príklade je však zadaná podmienka ukončenia, ale nikdy nie je splnená, ak je hodnota -1 sa odovzdáva vypočítaťFaktoriál () metóda, ktorá spôsobí neukončenú / nekonečnú rekurziu:

verejná trieda InfiniteRecursionWithTerminationCondition {public int CalcFactorial (int number) {return number == 1? 1: číslo * vypočítaťFaktoriál (číslo - 1); }}

Táto sada testov demonštruje tento scenár:

verejná trieda InfiniteRecursionWithTerminationConditionManualTest {@Test verejné neplatné danéPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = nový InfiniteRecursionWithTerminationCondition (); assertEquals (1, irtc.calculateFactorial (numToCalcFactorial)); } @Test public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = nový InfiniteRecursionWithTerminationCondition (); assertEquals (120, irtc.calculateFactorial (numToCalcFactorial)); } @Test (očakáva sa = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = nový InfiniteRecursionWithTerminationCondition (); irtc.calculateFactorial (numToCalcFactorial); }}

V tomto konkrétnom prípade by sa chybe dalo úplne zabrániť, keby bola podmienka ukončenia vyjadrená jednoducho takto:

verejná trieda RecursionWithCorrectTerminationCondition {verejné int vypočítaťFaktor (int číslo) {návratové číslo <= 1? 1: číslo * vypočítaťFaktoriál (číslo - 1); }}

Tu je test, ktorý ukazuje tento scenár v praxi:

verejná trieda RecursionWithCorrectTerminationConditionManualTest {@Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = nový RecursionWithCorrectTerminationCondition (); assertEquals (1, rctc.calculateFactorial (numToCalcFactorial)); }}

Teraz sa pozrime na scenár, kde StackOverflowError sa deje v dôsledku cyklických vzťahov medzi triedami. Uvažujme ClassOne a TriedaDva, ktoré sa navzájom vytvárajú vo svojich konštruktoroch a spôsobujú cyklický vzťah:

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; }}
verejná trieda 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; }}

Teraz povedzme, že sa pokúsime vytvoriť inštanciu ClassOne ako je vidieť v tomto teste:

verejná trieda CyclicDependancyManualTest {@Test (očakáva sa = StackOverflowError.class) verejná neplatnosť whenInstanciatingClassOne_thenThrowsException () {ClassOne obj = new ClassOne (); }}

Toto končí a StackOverflowError od staviteľa ClassOne je inštančný ClassTwo, a konštruktér TriedaDva opäť je inštancia ClassOne. A to sa deje opakovane, až kým pretečie zásobníkom.

Ďalej sa pozrieme na to, čo sa stane, keď sa trieda inštancuje v rámci tej istej triedy ako inštančná premenná tejto triedy.

Ako je vidieť v nasledujúcom príklade, Majiteľ účtu sa inštancuje ako premenná inštancie jointAccountHolder:

verejná trieda AccountHolder {private String firstName; private String priezvisko; AccountHolder jointAccountHolder = nový AccountHolder (); }

Keď Majiteľ účtu trieda je inštancovaná, a StackOverflowError je vyhodený kvôli rekurzívnemu volaniu konštruktora, ako je vidieť v tomto teste:

verejná trieda AccountHolderManualTest {@Test (očakáva sa = StackOverflowError.class) verejná neplatnosť whenInstanciatingAccountHolder_thenThrowsException () {Držiteľ účtuHolder = nový AccountHolder (); }}

4. Zaoberanie sa StackOverflowError

Najlepšie urobíte, keď a StackOverflowError narazíme, je opatrná kontrola stopy zásobníka, aby sa zistil opakujúci sa vzor čísel riadkov. Toto nám umožní nájsť kód, ktorý má problematickú rekurziu.

Poďme preskúmať niekoľko stohovacích zásobníkov spôsobených príkladmi kódu, ktoré sme videli už skôr.

Toto trasovanie zásobníka produkuje InfiniteRecursionWithTerminationConditionManualTest ak vynecháme očakávané vyhlásenie o výnimke:

java.lang.StackOverflowError na cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) na cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) na cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) v cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java : 5)

Tu je vidieť, že sa linka číslo 5 opakuje. Toto je miesto, kde sa uskutočňuje rekurzívne volanie. Teraz stačí len preskúmať kód a zistiť, či sa rekurzia deje správnym spôsobom.

Tu je stopa zásobníka, ktorú dostaneme vykonaním Cyklická závislosťManualTest (opäť bez očakávané výnimka):

java.lang.StackOverflowError pri c.b.s.ClassTwo. (ClassTwo.java:9) pri c.b.s.ClassOne. (ClassOne.java:9) pri c.b.s.ClassTwo. (ClassTwo.java:9) pri c.b.s.ClassOne. (ClassOne.java:9)

Toto trasovanie zásobníka zobrazuje čísla riadkov, ktoré spôsobujú problém v dvoch triedach, ktoré sú v cyklickom vzťahu. Riadok číslo 9 z TriedaDva a riadok číslo 9 ClassOne ukážte na miesto vo vnútri konštruktora, kde sa pokúša vytvoriť inštanciu druhej triedy.

Akonáhle je kód dôkladne skontrolovaný a ak nie je príčinou chyby nič z nasledujúceho (alebo iná logická chyba kódu):

  • Nesprávne implementovaná rekurzia (tj. Bez podmienky ukončenia)
  • Cyklická závislosť medzi triedami
  • Vytvorenie inštancie triedy v rámci tej istej triedy ako inštančná premenná tejto triedy

Bolo by dobré pokúsiť sa zväčšiť veľkosť zásobníka. V závislosti od nainštalovaného JVM sa predvolená veľkosť zásobníka môže líšiť.

The -Xss flag sa dá použiť na zväčšenie veľkosti zásobníka, či už z konfigurácie projektu alebo z príkazového riadku.

5. Záver

V tomto článku sme sa bližšie pozreli na StackOverflowError vrátane toho, ako to môže spôsobiť kód Java a ako ho môžeme diagnostikovať a opraviť.

Zdrojový kód súvisiaci s týmto článkom nájdete na GitHub.


$config[zx-auto] not found$config[zx-overlay] not found