Java >> Java tutorial >  >> Tag >> Stack

StackOverflowError i Java

1. Oversigt

StackOverflowError kan være irriterende for Java-udviklere, da det er en af ​​de mest almindelige runtime-fejl, vi kan støde på.

I denne artikel vil vi se, hvordan denne fejl kan opstå ved at se på en række kodeeksempler, samt hvordan vi kan håndtere det.

2. Stack Frames og hvordan StackOverflowError Forekommer

Lad os starte med det grundlæggende. Når en metode kaldes, oprettes en ny stakramme på opkaldsstakken. Denne stakramme indeholder parametre for den påkaldte metode, dens lokale variable og metodens returadresse, dvs. det punkt, hvorfra metodeudførelsen skal fortsætte, efter at den påkaldte metode er returneret.

Oprettelsen af ​​stak-rammer vil fortsætte, indtil den når slutningen af ​​metodeankaldelser fundet inde i indlejrede metoder.

Hvis JVM under denne proces støder på en situation, hvor der ikke er plads til at oprette en ny stackramme, vil den udsende en StackOverflowError .

Den mest almindelige årsag til, at JVM støder på denne situation er utermineret/uendelig rekursion – Javadoc-beskrivelsen for StackOverflowError nævner, at fejlen er smidt som et resultat af for dyb rekursion i et bestemt kodestykke.

Rekursion er dog ikke den eneste årsag til denne fejl. Det kan også ske i en situation, hvor en applikation beholder opkaldsmetoder indefra metoder, indtil stakken er opbrugt . Dette er et sjældent tilfælde, da ingen udviklere med vilje ville følge dårlig kodningspraksis. En anden sjælden årsag er at have et stort antal lokale variabler i en metode .

StackOverflowError kan også kastes, når en applikation er designet til at have c cykliske forhold mellem klasser . I denne situation bliver konstruktørerne af hinanden kaldt gentagne gange, hvilket får denne fejl til at blive kastet. Dette kan også betragtes som en form for rekursion.

Et andet interessant scenarie, der forårsager denne fejl, er, hvis en klasse bliver instansieret inden for samme klasse som en instansvariabel for den pågældende klasse . Dette vil få konstruktøren af ​​den samme klasse til at blive kaldt igen og igen (rekursivt), hvilket til sidst resulterer i en StackOverflowError.

I det næste afsnit vil vi se på nogle kodeeksempler, der viser disse scenarier.

3. StackOverflowError i aktion

I eksemplet vist nedenfor er en StackOverflowError vil blive smidt på grund af utilsigtet rekursion, hvor udvikleren har glemt at angive en opsigelsesbetingelse for den rekursive adfærd:

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

Her kastes fejlen ved alle lejligheder for en hvilken som helst værdi, der overføres til metoden:

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

I det næste eksempel er der dog angivet en opsigelsesbetingelse, men den bliver aldrig opfyldt, hvis en værdi på -1 videregives til calculateFactorial() metode, som forårsager utermineret/uendelig rekursion:

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

Dette sæt test viser dette scenarie:

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

I dette særlige tilfælde kunne fejlen have været fuldstændig undgået, hvis opsigelsesbetingelsen blot blev sat som:

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

Her er testen, der viser dette scenarie i praksis:

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

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

Lad os nu se på et scenarie, hvor StackOverflowError sker som følge af cykliske forhold mellem klasser. Lad os overveje ClassOne og Klasse 2 , som instansierer hinanden inde i deres konstruktører, hvilket forårsager en cyklisk sammenhæng:

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

Lad os nu sige, at vi prøver at instansiere ClassOne som det ses i denne test:

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

Dette ender med en StackOverflowError siden konstruktøren af ​​ClassOne instansierer KlasseTwo, og konstruktøren af ​​ClassTwo igen instansierer ClassOne. Og dette sker gentagne gange, indtil det flyder over stakken.

Dernæst vil vi se på, hvad der sker, når en klasse bliver instansieret inden for den samme klasse som en instansvariabel for den pågældende klasse.

Som det ses i det næste eksempel, Kontoindehaver instansierer sig selv som en instansvariabel jointAccountHolder :

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

Når Kontoindehaveren klasse er instansieret, en StackOverflowError kastes på grund af det rekursive kald af konstruktøren som set i denne test:

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

4. Håndtering af StackOverflowError

Den bedste ting at gøre, når en StackOverflowError er stødt på, er at inspicere stakken forsigtigt for at identificere det gentagne mønster af linjenumre. Dette vil gøre det muligt for os at finde den kode, der har problematisk rekursion.

Lad os undersøge et par stakspor forårsaget af de kodeeksempler, vi så tidligere.

Denne stak-sporing er produceret af InfiniteRecursionWithTerminationConditionManualTest hvis vi udelader det forventede undtagelseserklæring:

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)

Her kan linje nummer 5 ses gentages. Det er her, det rekursive kald foretages. Nu er det bare et spørgsmål om at undersøge koden for at se, om rekursionen er udført på en korrekt måde.

Her er staksporet, vi får ved at udføre CyclicDependancyManualTest (igen, uden forventet undtagelse):

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)

Denne stak-sporing viser linjenumrene, der forårsager problemet i de to klasser, der er i et cyklisk forhold. Linje nummer 9 i KlasseTwo og linje nummer 9 i ClassOne peg på placeringen inde i konstruktøren, hvor den forsøger at instansiere den anden klasse.

Når koden er blevet grundigt inspiceret, og hvis intet af følgende (eller nogen anden kodelogikfejl) er årsagen til fejlen:

  • Forkert implementeret rekursion (dvs. uden opsigelsesbetingelser)
  • Cyklisk afhængighed mellem klasser
  • Instantiering af en klasse inden for samme klasse som en forekomstvariabel for den pågældende klasse

Det ville være en god idé at forsøge at øge stakstørrelsen. Afhængigt af den installerede JVM kan standardstakstørrelsen variere.

-Xss flag kan bruges til at øge størrelsen af ​​stakken, enten fra projektets konfiguration eller kommandolinjen.

5. Konklusion

I denne artikel kiggede vi nærmere på StackOverflowError herunder hvordan Java-kode kan forårsage det, og hvordan vi kan diagnosticere og rette det.

Kildekode relateret til denne artikel kan findes på GitHub.


Java tag