Che cos'è un StackOverflowError
, cosa lo provoca e come devo gestirli?
new Object() {{getClass().newInstance();}};
ad un contesto statico (ad esempio main
metodo). Non funziona dal contesto dell'istanza (solo proiezioni InstantiationException
).
Che cos'è un StackOverflowError
, cosa lo provoca e come devo gestirli?
new Object() {{getClass().newInstance();}};
ad un contesto statico (ad esempio main
metodo). Non funziona dal contesto dell'istanza (solo proiezioni InstantiationException
).
Risposte:
I parametri e le variabili locali sono allocati nello stack (con i tipi di riferimento, l'oggetto vive nell'heap e una variabile nello stack fa riferimento a quell'oggetto nell'heap). Lo stack in genere vive all'estremità superiore dello spazio degli indirizzi e quando viene utilizzato si dirige verso la parte inferiore dello spazio degli indirizzi (ovvero verso lo zero).
Il tuo processo ha anche un mucchio , che vive nella parte inferiore del processo. Quando si alloca memoria, questo heap può crescere verso l'estremità superiore dello spazio degli indirizzi. Come puoi vedere, c'è un potenziale per l'heap di "scontrarsi" con lo stack (un po 'come le placche tettoniche !!!).
La causa comune di un overflow dello stack è una cattiva chiamata ricorsiva . In genere, ciò si verifica quando le funzioni ricorsive non hanno la condizione di terminazione corretta, quindi finisce per chiamarsi per sempre. O quando la condizione di terminazione va bene, può essere causata richiedendo troppe chiamate ricorsive prima di soddisfarla.
Tuttavia, con la programmazione della GUI, è possibile generare ricorsione indiretta . Ad esempio, l'app potrebbe gestire i messaggi di disegno e, durante l'elaborazione, potrebbe chiamare una funzione che fa sì che il sistema invii un altro messaggio di disegno. Qui non ti sei esplicitamente chiamato, ma il sistema operativo / VM lo ha fatto per te.
Per gestirli, devi esaminare il tuo codice. Se hai funzioni che si richiamano, verifica di avere una condizione di chiusura. Se sì, controlla che quando chiami la funzione hai almeno modificato uno degli argomenti, altrimenti non ci saranno cambiamenti visibili per la funzione chiamata ricorsivamente e la condizione finale è inutile. Ricorda inoltre che lo spazio dello stack può esaurire la memoria prima di raggiungere una condizione di terminazione valida, quindi assicurati che il tuo metodo sia in grado di gestire i valori di input che richiedono più chiamate ricorsive.
Se non hai ovvie funzioni ricorsive, controlla se stai chiamando funzioni di libreria che indirettamente causeranno la chiamata della tua funzione (come il caso implicito sopra).
Per descriverlo, dobbiamo prima capire come sono archiviate le variabili e gli oggetti locali .
Le variabili locali sono memorizzate nello stack :
Se guardassi l'immagine dovresti essere in grado di capire come funzionano le cose.
Quando una chiamata di funzione viene invocata da un'applicazione Java, uno stack frame viene allocato nello stack di chiamate. Il frame dello stack contiene i parametri del metodo richiamato, i suoi parametri locali e l'indirizzo di ritorno del metodo. L'indirizzo di ritorno indica il punto di esecuzione dal quale, l'esecuzione del programma deve continuare dopo il ritorno del metodo invocato. Se non c'è spazio per un nuovo frame dello stack, StackOverflowError
viene generato da Java Virtual Machine (JVM).
Il caso più comune che può esaurire lo stack di un'applicazione Java è la ricorsione. In ricorsione, un metodo si richiama durante la sua esecuzione. La ricorsione è considerata una potente tecnica di programmazione generale ma deve essere usata con cautela, per evitare StackOverflowError
.
Un esempio di lancio di a StackOverflowError
è mostrato di seguito:
StackOverflowErrorExample.java:
public class StackOverflowErrorExample {
public static void recursivePrint(int num) {
System.out.println("Number: " + num);
if (num == 0)
return;
else
recursivePrint(++num);
}
public static void main(String[] args) {
StackOverflowErrorExample.recursivePrint(1);
}
}
In questo esempio, definiamo un metodo ricorsivo, chiamato recursivePrint
che stampa un numero intero e quindi, chiama se stesso, con il numero intero successivo successivo come argomento. La ricorsione termina fino a quando non passiamo 0
come parametro. Tuttavia, nel nostro esempio, abbiamo passato il parametro da 1 e dai suoi follower in aumento, di conseguenza, la ricorsione non si interromperà mai.
Di -Xss1M
seguito viene mostrata un'esecuzione di esempio, utilizzando il flag che specifica la dimensione dello stack di thread pari a 1 MB:
Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackOverflowError
at java.io.PrintStream.write(PrintStream.java:480)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.write(PrintStream.java:527)
at java.io.PrintStream.print(PrintStream.java:669)
at java.io.PrintStream.println(PrintStream.java:806)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9)
...
A seconda della configurazione iniziale della JVM, i risultati possono differire, ma alla fine StackOverflowError
devono essere lanciati. Questo esempio è un ottimo esempio di come la ricorsione può causare problemi, se non implementata con cautela.
Come gestire StackOverflowError
La soluzione più semplice è ispezionare attentamente la traccia dello stack e rilevare lo schema ripetitivo dei numeri di riga. Questi numeri di riga indicano il codice chiamato in modo ricorsivo. Una volta rilevate queste righe, è necessario ispezionare attentamente il codice e capire perché la ricorsione non si interrompe mai.
Se hai verificato che la ricorsione è implementata correttamente, puoi aumentare le dimensioni dello stack, al fine di consentire un numero maggiore di invocazioni. A seconda della Java Virtual Machine (JVM) installata, la dimensione dello stack dei thread predefinita può essere pari a 512 KB o 1 MB . Puoi aumentare le dimensioni dello stack del thread usando il -Xss
flag. Questo flag può essere specificato tramite la configurazione del progetto o tramite la riga di comando. Il formato
-Xss
dell'argomento è:
-Xss<size>[g|G|m|M|k|K]
Se hai una funzione come:
int foo()
{
// more stuff
foo();
}
Quindi foo () continuerà a chiamarsi, sempre più in profondità, e quando lo spazio utilizzato per tenere traccia delle funzioni in cui ti trovi viene riempito, viene visualizzato l'errore di overflow dello stack.
Stack overflow significa esattamente questo: uno stack trabocca. Di solito c'è uno stack nel programma che contiene variabili e indirizzi di ambito locale dove restituire quando termina l'esecuzione di una routine. Tale stack tende ad essere un intervallo di memoria fisso da qualche parte nella memoria, quindi è limitato quanto può contenere valori.
Se lo stack è vuoto non è possibile pop, in caso contrario si verificherà un errore di underflow dello stack.
Se lo stack è pieno non è possibile inviare, in caso contrario si verificherà un errore di overflow dello stack.
Quindi lo overflow dello stack appare dove si alloca troppo nello stack. Ad esempio, nella citata ricorsione.
Alcune implementazioni ottimizzano alcune forme di ricorsione. Ricorsione della coda in particolare. Le routine ricorsive di coda sono una forma di routine in cui la chiamata ricorsiva appare come ultima cosa che fa la routine. Tale chiamata di routine viene semplicemente ridotta in un salto.
Alcune implementazioni arrivano fino a quando implementano le proprie pile per la ricorsione, quindi consentono alla ricorsione di continuare fino a quando il sistema esaurisce la memoria.
La cosa più semplice che potresti provare sarebbe di aumentare le dimensioni dello stack se puoi. Se non riesci a farlo, la seconda cosa migliore sarebbe guardare se c'è qualcosa che causa chiaramente lo overflow dello stack. Provalo stampando qualcosa prima e dopo la chiamata nella routine. Questo ti aiuta a scoprire la routine fallita.
Un overflow dello stack viene generalmente chiamato annidando le chiamate di funzione in modo troppo profondo (particolarmente semplice quando si utilizza la ricorsione, ovvero una funzione che chiama se stesso) o allocando una grande quantità di memoria nello stack in cui l'utilizzo dell'heap sarebbe più appropriato.
Come dici tu, devi mostrare un po 'di codice. :-)
Un errore di overflow dello stack si verifica in genere quando le chiamate di funzione annidano troppo in profondità. Vedi il thread Golf del codice di overflow dello stack per alcuni esempi di come ciò accade (anche se nel caso di quella domanda, le risposte causano intenzionalmente lo overflow dello stack).
La causa più comune di overflow dello stack è una ricorsione eccessivamente profonda o infinita . Se questo è il tuo problema, questo tutorial su Java Recursion potrebbe aiutarti a capire il problema.
StackOverflowError
è nello stack come OutOfMemoryError
è nell'heap.
Le chiamate ricorsive illimitate comportano l'esaurimento dello spazio dello stack.
L'esempio seguente produce StackOverflowError
:
class StackOverflowDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}
public static void main(String[] args)
{
unboundedRecursiveCall();
}
}
StackOverflowError
è evitabile se le chiamate ricorsive sono limitate per evitare che il totale aggregato delle chiamate in memoria incomplete (in byte) superi la dimensione dello stack (in byte).
Ecco un esempio di algoritmo ricorsivo per invertire un elenco collegato singolarmente. Su un laptop con le seguenti specifiche (memoria 4G, CPU Intel Core i5 2.3GHz, Windows 7 a 64 bit), questa funzione verrà eseguita nell'errore StackOverflow per un elenco collegato di dimensioni vicine a 10.000.
Il mio punto è che dovremmo usare la ricorsione in modo giudizioso, tenendo sempre conto delle dimensioni del sistema. Spesso la ricorsione può essere convertita in programma iterativo, che si ridimensiona meglio. (Una versione iterativa dello stesso algoritmo viene fornita in fondo alla pagina, inverte un elenco singolarmente collegato di dimensioni 1 milione in 9 millisecondi.)
private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){
LinkedListNode second = first.next;
first.next = x;
if(second != null){
return doReverseRecursively(first, second);
}else{
return first;
}
}
public static LinkedListNode reverseRecursively(LinkedListNode head){
return doReverseRecursively(null, head);
}
Versione iterativa dello stesso algoritmo:
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) {
while (first != null) {
LinkedListNode second = first.next;
first.next = x;
x = first;
if (second == null) {
break;
} else {
first = second;
}
}
return first;
}
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
A StackOverflowError
è un errore di runtime in Java.
Viene generato quando viene superata la quantità di memoria dello stack di chiamate allocata da JVM.
Un caso comune di StackOverflowError
essere lanciato, è quando lo stack di chiamate supera a causa di eccessiva ricorsione profonda o infinita.
Esempio:
public class Factorial {
public static int factorial(int n){
if(n == 1){
return 1;
}
else{
return n * factorial(n-1);
}
}
public static void main(String[] args){
System.out.println("Main method started");
int result = Factorial.factorial(-1);
System.out.println("Factorial ==>"+result);
System.out.println("Main method ended");
}
}
Traccia dello stack:
Main method started
Exception in thread "main" java.lang.StackOverflowError
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9)
Nel caso precedente, può essere evitato apportando modifiche programmatiche. Ma se la logica del programma è corretta e si verifica ancora, è necessario aumentare le dimensioni dello stack.
Questo è un tipico caso di java.lang.StackOverflowError
... Il metodo si chiama ricorsivamente senza uscire doubleValue()
,floatValue()
ecc
public class Rational extends Number implements Comparable<Rational> {
private int num;
private int denom;
public Rational(int num, int denom) {
this.num = num;
this.denom = denom;
}
public int compareTo(Rational r) {
if ((num / denom) - (r.num / r.denom) > 0) {
return +1;
} else if ((num / denom) - (r.num / r.denom) < 0) {
return -1;
}
return 0;
}
public Rational add(Rational r) {
return new Rational(num + r.num, denom + r.denom);
}
public Rational sub(Rational r) {
return new Rational(num - r.num, denom - r.denom);
}
public Rational mul(Rational r) {
return new Rational(num * r.num, denom * r.denom);
}
public Rational div(Rational r) {
return new Rational(num * r.denom, denom * r.num);
}
public int gcd(Rational r) {
int i = 1;
while (i != 0) {
i = denom % r.denom;
denom = r.denom;
r.denom = i;
}
return denom;
}
public String toString() {
String a = num + "/" + denom;
return a;
}
public double doubleValue() {
return (double) doubleValue();
}
public float floatValue() {
return (float) floatValue();
}
public int intValue() {
return (int) intValue();
}
public long longValue() {
return (long) longValue();
}
}
public class Main {
public static void main(String[] args) {
Rational a = new Rational(2, 4);
Rational b = new Rational(2, 6);
System.out.println(a + " + " + b + " = " + a.add(b));
System.out.println(a + " - " + b + " = " + a.sub(b));
System.out.println(a + " * " + b + " = " + a.mul(b));
System.out.println(a + " / " + b + " = " + a.div(b));
Rational[] arr = {new Rational(7, 1), new Rational(6, 1),
new Rational(5, 1), new Rational(4, 1),
new Rational(3, 1), new Rational(2, 1),
new Rational(1, 1), new Rational(1, 2),
new Rational(1, 3), new Rational(1, 4),
new Rational(1, 5), new Rational(1, 6),
new Rational(1, 7), new Rational(1, 8),
new Rational(1, 9), new Rational(0, 1)};
selectSort(arr);
for (int i = 0; i < arr.length - 1; ++i) {
if (arr[i].compareTo(arr[i + 1]) > 0) {
System.exit(1);
}
}
Number n = new Rational(3, 2);
System.out.println(n.doubleValue());
System.out.println(n.floatValue());
System.out.println(n.intValue());
System.out.println(n.longValue());
}
public static <T extends Comparable<? super T>> void selectSort(T[] array) {
T temp;
int mini;
for (int i = 0; i < array.length - 1; ++i) {
mini = i;
for (int j = i + 1; j < array.length; ++j) {
if (array[j].compareTo(array[mini]) < 0) {
mini = j;
}
}
if (i != mini) {
temp = array[i];
array[i] = array[mini];
array[mini] = temp;
}
}
}
}
2/4 + 2/6 = 4/10
Exception in thread "main" java.lang.StackOverflowError
2/4 - 2/6 = 0/-2
at com.xetrasu.Rational.doubleValue(Rational.java:64)
2/4 * 2/6 = 4/24
at com.xetrasu.Rational.doubleValue(Rational.java:64)
2/4 / 2/6 = 12/8
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
Ecco un esempio
public static void main(String[] args) {
System.out.println(add5(1));
}
public static int add5(int a) {
return add5(a) + 5;
}
Uno StackOverflowError è fondamentalmente quando si tenta di fare qualcosa, che molto probabilmente si chiama da solo, e continua all'infinito (o fino a quando non si ottiene uno StackOverflowError).
add5(a)
chiamerà se stesso, quindi chiamerà di nuovo se stesso e così via.
Il termine "stack overrun (overflow)" è spesso usato ma un termine improprio; gli attacchi non traboccano dallo stack ma dai buffer nello stack.
- da diapositive del Prof. Dr. Dieter Gollmann