StackOverflowError in Java

1. Panoramica

StackOverflowError può essere fastidioso per gli sviluppatori Java, poiché è uno degli errori di runtime più comuni che possiamo riscontrare.

In questo articolo, vedremo come può verificarsi questo errore esaminando una varietà di esempi di codice e come possiamo affrontarlo.

2. Stack Frames e come si verifica StackOverflowError

Cominciamo con le basi. Quando viene chiamato un metodo, viene creato un nuovo stack frame nello stack di chiamate. Questo stack frame contiene i parametri del metodo invocato, le sue variabili locali e l'indirizzo di ritorno del metodo, cioè il punto da cui l'esecuzione del metodo dovrebbe continuare dopo che il metodo invocato è tornato.

La creazione di stack frame continuerà fino a raggiungere la fine delle chiamate di metodo trovate all'interno di metodi annidati.

Durante questo processo, se JVM incontra una situazione in cui non c'è spazio per creare un nuovo stack frame, lancerà un'eccezione StackOverflowError .

La causa più comune per cui JVM incontra questa situazione è la ricorsione infinita / non terminata : la descrizione Javadoc per StackOverflowError menziona che l'errore viene generato a causa di una ricorsione troppo profonda in un particolare frammento di codice.

Tuttavia, la ricorsione non è l'unica causa di questo errore. Può anche verificarsi in una situazione in cui un'applicazione continua a chiamare metodi dall'interno dei metodi finché lo stack non è esaurito . Questo è un caso raro poiché nessuno sviluppatore seguirebbe intenzionalmente cattive pratiche di codifica. Un'altra causa rara è avere un vasto numero di variabili locali all'interno di un metodo .

Lo StackOverflowError può essere lanciato anche quando un'applicazione è progettato per avere c relazioni yclic tra le classi . In questa situazione, i costruttori l'uno dell'altro vengono chiamati ripetutamente, il che causa la generazione di questo errore. Questo può anche essere considerato come una forma di ricorsione.

Un altro scenario interessante che causa questo errore è se una classe viene istanziata all'interno della stessa classe come variabile di istanza di quella classe . Ciò farà sì che il costruttore della stessa classe venga chiamato ancora e ancora (in modo ricorsivo) che alla fine si traduce in uno StackOverflowError.

Nella sezione successiva, esamineremo alcuni esempi di codice che illustrano questi scenari.

3. StackOverflowError in azione

Nell'esempio mostrato di seguito, verrà lanciato un StackOverflowError a causa di una ricorsione non intenzionale, in cui lo sviluppatore ha dimenticato di specificare una condizione di terminazione per il comportamento ricorsivo:

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

Qui, l'errore viene generato in tutte le occasioni per qualsiasi valore passato nel metodo:

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

Tuttavia, nel prossimo esempio viene specificata una condizione di terminazione ma non viene mai soddisfatta se un valore di -1 viene passato al metodo calcolaFactorial () , che causa una ricorsione non terminata / infinita:

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

Questa serie di test dimostra questo scenario:

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

In questo caso particolare, l'errore avrebbe potuto essere completamente evitato se la condizione di terminazione fosse stata semplicemente posta come:

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

Ecco il test che mostra questo scenario nella pratica:

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

Ora diamo un'occhiata a uno scenario in cui StackOverflowError si verifica come risultato di relazioni cicliche tra classi. Consideriamo ClassOne e ClassTwo , che si istanziano a vicenda all'interno dei rispettivi costruttori provocando una relazione ciclica:

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

Ora diciamo di provare a istanziare ClassOne come visto in questo test:

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

Questo finisce con un StackOverflowError poiché il costruttore di ClassOne crea un'istanza di ClassTwo e il costruttore di ClassTwo sta nuovamente istanziando ClassOne. E questo accade ripetutamente fino a quando non supera lo stack.

Successivamente, vedremo cosa succede quando una classe viene istanziata all'interno della stessa classe come variabile di istanza di quella classe.

Come si vede nel prossimo esempio, AccountHolder crea un'istanza come variabile di istanza jointAccountHolder :

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

Quando l' intestatario della classe viene creata un'istanza , uno StackOverflowError è gettato a causa della chiamata ricorsiva del costruttore come si vede in questo test:

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

4. Trattare con StackOverflowError

La cosa migliore da fare quando si incontra un StackOverflowError è ispezionare con cautela la traccia dello stack per identificare lo schema ripetuto dei numeri di riga. Questo ci consentirà di individuare il codice che presenta una ricorsione problematica.

Esaminiamo alcune tracce dello stack causate dagli esempi di codice che abbiamo visto in precedenza.

Questa traccia dello stack viene prodotta da InfiniteRecursionWithTerminationConditionManualTest se omettiamo la dichiarazione di eccezione prevista :

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)

Qui, la riga numero 5 può essere vista ripetersi. È qui che viene eseguita la chiamata ricorsiva. Ora è solo questione di esaminare il codice per vedere se la ricorsione viene eseguita in modo corretto.

Ecco la traccia dello stack che otteniamo eseguendo CyclicDependancyManualTest (di nuovo, senza eccezioni previste ):

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)

Questa analisi dello stack mostra i numeri di riga che causano il problema nelle due classi che sono in una relazione ciclica. La riga numero 9 di ClassTwo e la riga numero 9 di ClassOne puntano alla posizione all'interno del costruttore in cui tenta di creare un'istanza dell'altra classe.

Una volta che il codice è stato accuratamente ispezionato e se nessuna delle seguenti (o qualsiasi altro errore di logica del codice) è la causa dell'errore:

  • Ricorsione implementata in modo errato (ovvero senza condizione di terminazione)
  • Dipendenza ciclica tra classi
  • Istanziare una classe all'interno della stessa classe come variabile di istanza di quella classe

Sarebbe una buona idea provare ad aumentare la dimensione dello stack. A seconda della JVM installata, la dimensione dello stack predefinita potrebbe variare.

Il flag -Xss può essere utilizzato per aumentare la dimensione dello stack, dalla configurazione del progetto o dalla riga di comando.

5. conclusione

In questo articolo, abbiamo esaminato più da vicino StackOverflowError, incluso il modo in cui il codice Java può causarlo e come possiamo diagnosticare e risolverlo.

Il codice sorgente relativo a questo articolo può essere trovato su GitHub.