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 såvel som hvordan vi kan håndtere den.

2. Stakrammer og hvordan StackOverflowError Opstår

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

Oprettelsen af ​​stakrammer vil fortsætte, indtil den når slutningen af ​​metodeopkald, der findes i indlejrede metoder.

I løbet af denne proces, hvis JVM støder på en situation, hvor der ikke er plads til en ny stakramme, der skal oprettes, vil det kaste et StackOverflowError.

Den mest almindelige årsag til, at JVM støder på denne situation er ubestemt / uendelig rekursion - Javadoc-beskrivelsen til StackOverflowError nævner, at fejlen smides 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 ansøgning opbevares kalder metoder indefra metoder, indtil stakken er opbrugt. Dette er et sjældent tilfælde, da ingen udviklere med vilje vil følge dårlige kodningsmetoder. En anden sjælden årsag er har et stort antal lokale variabler inde i en metode.

Det StackOverflowError kan også kastes, når en applikation er designet til at have 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 scenario, der forårsager denne fejl, er, hvis en klasse instantieres inden for samme klasse som en instansvariabel for den klasse. Dette vil medføre, at konstruktøren i samme klasse kaldes igen og igen (rekursivt), hvilket til sidst resulterer i a StackOverflowError.

I det næste afsnit ser vi på nogle kodeeksempler, der demonstrerer disse scenarier.

3. StackOverflowError i aktion

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

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

Her kastes fejlen ved alle lejligheder for enhver værdi, der overføres til metoden:

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

I det næste eksempel er der imidlertid angivet en opsigelsesbetingelse, men den bliver aldrig opfyldt, hvis en værdi på -1 overføres til beregneFaktor () metode, der forårsager uafsluttet / uendelig rekursion:

offentlig klasse InfiniteRecursionWithTerminationCondition {offentlig int beregneFaktorisk (int-nummer) {returnummer == 1? 1: antal * beregneFaktorisk (nummer - 1); }}

Dette sæt tests demonstrerer dette scenario:

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

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

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

Her er testen, der viser dette scenario i praksis:

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

Lad os nu se på et scenario, hvor StackOverflowError sker som et resultat af cykliske forhold mellem klasser. Lad os overveje ClassOne og ClassTwo, som instantierer hinanden inde i deres konstruktører, der forårsager et cyklisk 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 {privat int toVærdi; private ClassOne clsOneInstance = null; offentlig ClassTwo () {twoValue = 10; clsOneInstance = ny ClassOne (); } offentlig ClassTwo (int twoValue, ClassOne clsOneInstance) {this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; }}

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

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

Dette ender med en StackOverflowError siden konstruktøren af ClassOne er øjeblikkelig Klasse to, og konstruktøren af ClassTwo igen er øjeblikkelig ClassOne. Og dette sker gentagne gange, indtil det løber over stakken.

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

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

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

Når Kontoindehaver klassen er instantieret, -en StackOverflowError kastes på grund af konstruktørens rekursive kald som vist i denne test:

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

4. At håndtere StackOverflowError

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

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

Dette stakspor er produceret af InfiniteRecursionWithTerminationConditionManualTest hvis vi udelader forventet undtagelseserklæring:

java.lang.StackOverflowError på 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 linie nummer 5 ses gentage. Det er her, det rekursive opkald foretages. Nu er det bare et spørgsmål om at undersøge koden for at se, om rekursion sker på en korrekt måde.

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

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 staksporing viser linienumrene, der forårsager problemet i de to klasser, der er i et cyklisk forhold. Linie nummer 9 af ClassTwo og linje nummer 9 i ClassOne peg på placeringen inde i konstruktøren, hvor den forsøger at instantiere den anden klasse.

Når koden er grundigt inspiceret, og hvis ingen af ​​følgende (eller andre kodelogiske fejl) 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 instansvariabel for den pågældende klasse

Det ville være en god ide at prøve at øge stakkens størrelse. Afhængigt af den installerede JVM kan standardstørrelsen på stakken variere.

Det -Xss flag kan bruges til at øge størrelsen på 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.