StackOverflowError i Java

1. Oversikt

StackOverflowError kan være irriterende for Java-utviklere, da det er en av de vanligste kjøretidsfeilene vi kan støte på.

I denne artikkelen vil vi se hvordan denne feilen kan oppstå ved å se på en rekke kodeeksempler, samt hvordan vi kan håndtere den.

2. Stabelrammer og hvordan StackOverflowError Inntreffer

La oss starte med det grunnleggende. Når en metode kalles, opprettes en ny stabelramme på samtalestakken. Denne stabelrammen inneholder parametere for den påkalte metoden, dens lokale variabler og returadressen til metoden, dvs. det punktet hvor metodeutførelsen skal fortsette etter at den påkalte metoden har kommet tilbake.

Opprettelsen av stabelrammer vil fortsette til den når slutten av metodeanropninger som finnes i nestede metoder.

Hvis JVM møter en situasjon der det ikke er plass til å opprette en ny stabelramme, vil den kaste en StackOverflowError.

Den vanligste årsaken til at JVM møter denne situasjonen er ubestemt / uendelig rekursjon - Javadoc-beskrivelsen for StackOverflowError nevner at feilen blir kastet som et resultat av for dyp rekursjon i et bestemt kodebit.

Rekursjon er imidlertid ikke den eneste årsaken til denne feilen. Det kan også skje i en situasjon der en søknad beholder anropsmetoder innenfra metoder til stakken er oppbrukt. Dette er et sjeldent tilfelle siden ingen utviklere med vilje vil følge dårlig kodingspraksis. En annen sjelden årsak er har et stort antall lokale variabler inne i en metode.

De StackOverflowError kan også kastes når et program er designet for å ha cykliske forhold mellom klassene. I denne situasjonen blir konstruktørene av hverandre kalt gjentatte ganger, noe som får denne feilen til å bli kastet. Dette kan også betraktes som en form for rekursjon.

Et annet interessant scenario som forårsaker denne feilen, er hvis en klasse blir instansert i samme klasse som en instansvariabel for den klassen. Dette vil føre til at konstruktøren i samme klasse blir kalt igjen og igjen (rekursivt), noe som til slutt resulterer i a StackOverflowError.

I neste avsnitt ser vi på noen kodeeksempler som viser disse scenariene.

3. StackOverflowError i aksjon

I eksemplet vist nedenfor, a StackOverflowError vil bli kastet på grunn av utilsiktet rekursjon, der utvikleren har glemt å spesifisere en avslutningsbetingelse for den rekursive oppførselen:

offentlig klasse UnintendedInfiniteRecursion {offentlig int beregneFaktor (int nummer) {retur nummer * beregneFaktor (nummer - 1); }}

Her kastes feilen ved alle anledninger for enhver verdi som sendes til metoden:

offentlig klasse UnintendedInfiniteRecursionManualTest {@Test (forventet = StackOverflowError.class) offentlig ugyldig givenPositiveIntNoOne_whenCalFact_thenThrowsException () {int numToCalcFactorial = 1; UnintendedInfiniteRecursion uir = ny UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (forventet = StackOverflowError.class) offentlig ugyldig givenPositiveIntGtOne_whenCalcFact_thenThrowsException () {int numToCalcFactorial = 2; UnintendedInfiniteRecursion uir = ny UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (forventet = StackOverflowError.class) offentlig tomrom gittNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; UnintendedInfiniteRecursion uir = ny UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); }}

I neste eksempel er imidlertid en oppsigelsesbetingelse spesifisert, men blir aldri oppfylt hvis verdien på -1 blir overført til beregneFaktor () metode som forårsaker ubestemt / uendelig rekursjon:

offentlig klasse InfiniteRecursionWithTerminationCondition {offentlig int beregneFaktor (int nummer) {retur nummer == 1? 1: tall * beregneFaktorisk (nummer - 1); }}

Dette settet med tester demonstrerer dette scenariet:

offentlig klasse InfiniteRecursionWithTerminationConditionManualTest {@Test public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition (); assertEquals (1, irtc.calculateFactorial (numToCalcFactorial)); } @Test offentlig ugyldig givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition (); assertEquals (120, irtc.calculateFactorial (numToCalcFactorial)); } @Test (forventet = StackOverflowError.class) offentlig tomrom gittNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition (); irtc.calculateFactorial (numToCalcFactorial); }}

I dette spesielle tilfellet kunne feilen ha vært helt unngått hvis avslutningsbetingelsen rett og slett ble satt som:

offentlig klasse RecursionWithCorrectTerminationCondition {offentlig int beregneFaktor (int nummer) {retur nummer <= 1? 1: tall * beregneFaktorisk (nummer - 1); }}

Her er testen som viser dette scenariet i praksis:

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

La oss nå se på et scenario der StackOverflowError skjer som et resultat av sykliske forhold mellom klassene. La oss vurdere ClassOne og ClassTwo, som instantierer hverandre inne i konstruktørene og forårsaker et syklisk forhold:

offentlig klasse ClassOne {privat int oneValue; privat ClassTwo clsTwoInstance = null; offentlig ClassOne () {oneValue = 0; clsTwoInstance = ny ClassTwo (); } offentlig ClassOne (int oneValue, ClassTwo clsTwoInstance) {this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; }}
offentlig klasse ClassTwo {private int twoValue; private ClassOne clsOneInstance = null; offentlig ClassTwo () {twoValue = 10; clsOneInstance = ny ClassOne (); } offentlig ClassTwo (int twoValue, ClassOne clsOneInstance) {this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; }}

La oss si at vi prøver å instantiere ClassOne som sett i denne testen:

offentlig klasse CyclicDependancyManualTest {@Test (forventet = StackOverflowError.class) offentlig ugyldig nårInstanciatingClassOne_thenThrowsException () {ClassOne obj = new ClassOne (); }}

Dette ender opp med en StackOverflowError siden konstruktøren av ClassOne er øyeblikkelig Klasse to, og konstruktøren av ClassTwo igjen er øyeblikkelig ClassOne. Og dette skjer gjentatte ganger til den renner over bunken.

Deretter vil vi se på hva som skjer når en klasse instanseres i samme klasse som en instansvariabel i den klassen.

Som det fremgår av neste eksempel, Kontoinnehaver instantierer seg selv som en instansvariabel jointAccountHolder:

offentlig klasse AccountHolder {privat streng fornavn; privat streng etternavn; AccountHolder jointAccountHolder = ny AccountHolder (); }

Når Kontoinnehaver klassen er instantiert, en StackOverflowError blir kastet på grunn av konstruktørens rekursive kall som vist i denne testen:

offentlig klasse AccountHolderManualTest {@Test (forventet = StackOverflowError.class) offentlig ugyldig nårInstanciatingAccountHolder_thenThrowsException () {AccountHolder holder = new AccountHolder (); }}

4. Å takle StackOverflowError

Den beste tingen å gjøre når en StackOverflowError er oppstått er å inspisere stakkesporet forsiktig for å identifisere det gjentatte mønsteret for linjenumre. Dette vil gjøre det mulig for oss å finne koden som har problematisk rekursjon.

La oss undersøke noen få stakkspor forårsaket av kodeeksemplene vi så tidligere.

Dette stabelsporet er produsert av InfiniteRecursionWithTerminationConditionManualTest hvis vi utelater forventet unntakserklæring:

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

Her kan linje nummer 5 sees gjentatt. Det er her den rekursive samtalen blir gjort. Nå er det bare å undersøke koden for å se om rekursjonen gjøres på en riktig måte.

Her er stakkesporet vi får ved å utføre CyclicDependancyManualTest (igjen, uten forventet unntak):

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.ClassOne. (ClassOne.java:9)

Denne stabelsporingen viser linjenumrene som forårsaker problemet i de to klassene som er i et syklisk forhold. Linje nummer 9 av ClassTwo og linje nummer 9 av ClassOne pek på stedet inne i konstruktøren der den prøver å instansiere den andre klassen.

Når koden er grundig inspisert, og hvis ingen av følgende (eller andre kodelogiske feil) er årsaken til feilen:

  • Feil implementert rekursjon (dvs. uten avslutningsbetingelser)
  • Syklisk avhengighet mellom klassene
  • Instantiering av en klasse i samme klasse som en instansvariabel for den klassen

Det vil være lurt å prøve å øke stabelstørrelsen. Avhengig av JVM installert, kan standard stabelstørrelse variere.

De -Xss flagg kan brukes til å øke størrelsen på stakken, enten fra prosjektets konfigurasjon eller kommandolinjen.

5. Konklusjon

I denne artikkelen så vi nærmere på StackOverflowError inkludert hvordan Java-kode kan forårsake det, og hvordan vi kan diagnostisere og fikse det.

Kildekode relatert til denne artikkelen finner du på GitHub.


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