StackOverflowError u Javi

1. Pregled

StackOverflowError može smetati programerima Jave, jer je to jedna od najčešćih pogrešaka u izvođenju koje možemo susresti.

U ovom ćemo članku vidjeti kako se ova pogreška može dogoditi gledajući razne primjere koda kao i kako se možemo nositi s njom.

2. Okviri za slaganje i kako StackOverflowError Javlja se

Krenimo s osnovama. Kada se metoda pozove, na stogu poziva kreira se novi okvir steka. Ovaj okvir steka sadrži parametre pozvane metode, njene lokalne varijable i povratnu adresu metode, tj. Točku od koje bi se izvršenje metode trebalo nastaviti nakon povratka pozvane metode.

Stvaranje okvira snopa nastavit će se dok ne dosegne kraj poziva metode koji se nalaze unutar ugniježđenih metoda.

Tijekom ovog postupka, ako JVM naiđe na situaciju u kojoj nema mjesta za stvaranje novog okvira stoga, bacit će StackOverflowError.

Najčešći uzrok da se JVM susretne s ovom situacijom je nedorečena / beskonačna rekurzija - opis Javadoc za StackOverflowError spominje da je pogreška bačena kao rezultat preduboke rekurzije u određenom isječku koda.

Međutim, rekurzija nije jedini uzrok ove pogreške. To se također može dogoditi u situaciji u kojoj se aplikacija zadržava pozivanje metoda iznutra do iscrpljenja steka. To je rijedak slučaj jer niti jedan programer ne bi namjerno slijedio loše prakse kodiranja. Još jedan rijedak uzrok je koji imaju ogroman broj lokalnih varijabli unutar metode.

The StackOverflowError može se baciti i kada je aplikacija dizajnirana da ima cyclic odnosi između klasa. U ovoj se situaciji međusobno pozivaju konstruktori koji se ponavljaju, što uzrokuje izbacivanje ove pogreške. To se također može smatrati oblikom rekurzije.

Još jedan zanimljiv scenarij koji uzrokuje ovu pogrešku je ako a klasa se instancira unutar iste klase kao varijabla instance te klase. To će dovesti do toga da se konstruktor iste klase poziva iznova i iznova (rekurzivno) što na kraju rezultira a StackOverflowError.

U sljedećem ćemo odjeljku pogledati neke primjere koda koji pokazuju ove scenarije.

3. StackOverflowError na djelu

U dolje prikazanom primjeru, a StackOverflowError bit će bačen zbog nenamjerne rekurzije, gdje je programer zaboravio navesti uvjet prekida za rekurzivno ponašanje:

javna klasa UnintendedInfiniteRecursion {javni int CalcuFactorial (int broj) {povratni broj * izračunajFactorial (broj - 1); }}

Ovdje se pogreška baca u svim prilikama za bilo koju vrijednost prenesenu u metodu:

javna klasa UnintendedInfiniteRecursionManualTest {@Test (očekuje se = StackOverflowError.class) javna praznina givenPositiveIntNoOne_whenCalFact_thenThrowsException () {int numToCalcFactorial = 1; UnintendedInfiniteRecursion uir = novi UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (očekuje se = StackOverflowError.class) javna praznina givenPositiveIntGtOne_whenCalcFact_thenThrowsException () {int numToCalcFactorial = 2; UnintendedInfiniteRecursion uir = novi UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (očekuje se = StackOverflowError.class) javna praznina givenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; UnintendedInfiniteRecursion uir = novi UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); }}

Međutim, u sljedećem je primjeru naveden uvjet raskida, ali nikada nije zadovoljen ako je vrijednost od -1 se prosljeđuje izračunatiFactorial () metoda koja uzrokuje nedodijeljenu / beskonačnu rekurziju:

javna klasa InfiniteRecursionWithTerminationCondition {public int izračunajFaktorijal (int broj) {return broj == 1? 1: broj * izračunajFaktorijal (broj - 1); }}

Ovaj niz testova pokazuje ovaj scenarij:

javna klasa InfiniteRecursionWithTerminationConditionManualTest {@Test javna praznina givenPositiveIntNoOne_whenCalcFact_thenCorrectCalc () {int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = novo InfiniteRecursionWithTerminationCondition (); assertEquals (1, irtc.calculateFactorial (numToCalcFactorial)); } @Test javna praznina givenPositiveIntGtOne_whenCalcFact_thenCorrectCalc () {int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = novo InfiniteRecursionWithTerminationCondition (); assertEquals (120, irtc.calculateFactorial (numToCalcFactorial)); } @Test (očekuje se = StackOverflowError.class) javna praznina givenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = novo InfiniteRecursionWithTerminationCondition (); irtc.calculateFactorial (numToCalcFactorial); }}

U ovom konkretnom slučaju, pogreška se mogla potpuno izbjeći da se uvjet raskida jednostavno stavi kao:

javna klasa RecursionWithCorrectTerminationCondition {javni int izračunFactorial (int broj) {povratni broj <= 1? 1: broj * izračunajFaktorijal (broj - 1); }}

Evo testa koji pokazuje ovaj scenarij u praksi:

javna klasa RecursionWithCorrectTerminationConditionManualTest {@Test javna praznina givenNegativeInt_whenCalcFact_thenCorrectCalc () {int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = novo RecursionWithCorrectTerminationCondition (); assertEquals (1, rctc.calculateFactorial (numToCalcFactorial)); }}

Sada pogledajmo scenarij u kojem StackOverflowError događa kao rezultat cikličkih odnosa između klasa. Razmotrimo ClassOne i Razred dva, koji se međusobno instantiraju unutar svojih konstruktora uzrokujući ciklički odnos:

javna klasa ClassOne {private int oneValue; private ClassTwo clsTwoInstance = null; javni ClassOne () {oneValue = 0; clsTwoInstance = nova ClassTwo (); } javni ClassOne (int oneValue, ClassTwo clsTwoInstance) {this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; }}
javna klasa ClassTwo {private int twoValue; private ClassOne clsOneInstance = null; javni ClassTwo () {twoValue = 10; clsOneInstance = novi ClassOne (); } javna ClassTwo (int twoValue, ClassOne clsOneInstance) {this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; }}

Sad recimo da pokušavamo instancirati ClassOne kao što se vidi u ovom testu:

javna klasa CyclicDependancyManualTest {@Test (očekuje se = StackOverflowError.class) javna praznina kadaInstanciatingClassOne_thenThrowsException () {ClassOne obj = new ClassOne (); }}

Ovo završava s StackOverflowError budući da je konstruktor ClassOne je instanciranje Razred dva, i konstruktor Razred dva opet je instanciranje ClassOne. I to se opetovano događa dok ne prelije stack.

Zatim ćemo pogledati što se događa kada se klasa instancira unutar iste klase kao varijabla instance te klase.

Kao što se vidi u sljedećem primjeru, Nositelj računa instancira se kao varijabla instance jointAccountHolder:

javna klasa AccountHolder {private String firstName; private String lastName; AccountHolder jointAccountHolder = novi AccountHolder (); }

Kada Nositelj računa klasa je instancirana, a StackOverflowError baca se zbog rekurzivnog poziva konstruktora kao što se vidi u ovom testu:

javna klasa AccountHolderManualTest {@Test (očekuje se = StackOverflowError.class) javna praznina whenInstanciatingAccountHolder_thenThrowsException () {Vlasnik AccountHolder = novi AccountHolder (); }}

4. Suočavanje s StackOverflowError

Najbolje što treba učiniti kad StackOverflowError je oprezno pregledati trag stoga kako bi se identificirao ponavljajući obrazac brojeva linija. To će nam omogućiti lociranje koda koji ima problematičnu rekurziju.

Ispitajmo nekoliko tragova stoga uzrokovanih primjerima koda koje smo ranije vidjeli.

Ovaj trag stoga proizvodi InfiniteRecursionWithTerminationConditionManualTest ako izostavimo očekivano deklaracija o iznimci:

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

Ovdje se može vidjeti redak broj 5 kako se ponavlja. Ovdje se vrši rekurzivni poziv. Sada je samo pitanje ispitivanja koda kako bi se utvrdilo je li rekurzija izvedena na ispravan način.

Evo traga stoga koji dobivamo izvršavanjem CyclicDependancyManualTest (opet, bez očekivano iznimka):

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

Ovaj trag steka prikazuje brojeve redaka koji uzrokuju problem u dvije klase koje su u cikličkom odnosu. Redak broj 9 od Razred dva i redak broj 9 ClassOne pokažite na mjesto unutar konstruktora gdje pokušava instancirati drugu klasu.

Nakon što se kod temeljito pregleda i ako ništa od sljedećeg (ili bilo koja druga logička pogreška koda) nije uzrok pogreške:

  • Pogrešno implementirana rekurzija (tj. Bez uvjeta prekida)
  • Ciklična ovisnost između predavanja
  • Instanciranje klase unutar iste klase kao varijable instance te klase

Bilo bi dobro pokušati povećati veličinu stoga. Ovisno o instaliranom JVM-u, zadana veličina sloga može se razlikovati.

The -Xss zastava se može koristiti za povećanje veličine stoga, bilo iz konfiguracije projekta ili iz naredbenog retka.

5. Zaključak

U ovom smo članku pobliže pogledali StackOverflowError uključujući kako Java kôd to može uzrokovati i kako ga možemo dijagnosticirati i popraviti.

Izvorni kod povezan s ovim člankom možete pronaći na GitHubu.