Perché questo metodo stampa 4?


111

Mi chiedevo cosa succede quando provi a catturare un StackOverflowError e hai trovato il seguente metodo:

class RandomNumberGenerator {

    static int cnt = 0;

    public static void main(String[] args) {
        try {
            main(args);
        } catch (StackOverflowError ignore) {
            System.out.println(cnt++);
        }
    }
}

Ora la mia domanda:

Perché questo metodo stampa "4"?

Ho pensato che forse fosse perché ha System.out.println()bisogno di 3 segmenti nello stack di chiamate, ma non so da dove viene il numero 3. Quando guardi il codice sorgente (e il bytecode) di System.out.println(), normalmente porterebbe a molte più chiamate di metodo di 3 (quindi 3 segmenti sullo stack di chiamate non sarebbero sufficienti). Se è a causa delle ottimizzazioni applicate dalla VM Hotspot (metodo inlining), mi chiedo se il risultato sarebbe diverso su un'altra VM.

Modifica :

Poiché l'output sembra essere altamente specifico per JVM, ottengo il risultato 4 utilizzando
Java (TM) SE Runtime Environment (build 1.6.0_41-b02)
Java HotSpot (TM) VM server a 64 bit (build 20.14-b01, modalità mista)


Spiegazione del motivo per cui penso che questa domanda sia diversa dalla comprensione dello stack Java :

La mia domanda non riguarda il motivo per cui c'è un cnt> 0 (ovviamente perché System.out.println()richiede la dimensione dello stack e ne lancia un altro StackOverflowErrorprima che qualcosa venga stampato), ma perché ha il valore particolare di 4, rispettivamente 0,3,8,55 o qualcos'altro su altro sistemi.


4
Nel mio locale, ricevo "0" come previsto.
Reddy

2
Questo può occuparsi di molte cose di architettura. Quindi è meglio pubblicare il tuo output con la versione jdk Per me l'output è 0 su jdk 1.7
Lokesh

3
Ho ottenuto 5, 6e 38con Java 1.7.0_10
Kon

8
@Elist non sarà lo stesso output quando esegui trucchi che coinvolgono l'architettura sottostante;)
m0skit0

3
@flrnb È solo uno stile che uso per allineare le parentesi graffe. Mi rende più facile sapere dove iniziano e finiscono le condizioni e le funzioni. Puoi cambiarlo se vuoi, ma secondo me è più leggibile in questo modo.
syb0rg

Risposte:


41

Penso che gli altri abbiano fatto un buon lavoro nello spiegare perché cnt> 0, ma non ci sono abbastanza dettagli sul perché cnt = 4 e perché cnt varia così ampiamente tra le diverse impostazioni. Cercherò di riempire quel vuoto qui.

Permettere

  • X è la dimensione totale dello stack
  • M è lo spazio dello stack utilizzato quando entriamo in main la prima volta
  • R è l'aumento dello spazio dello stack ogni volta che entriamo in main
  • P è lo spazio dello stack necessario per eseguire System.out.println

Quando entriamo per la prima volta in main, lo spazio rimasto è XM. Ogni chiamata ricorsiva occupa R più memoria. Quindi per 1 chiamata ricorsiva (1 in più dell'originale), l'uso della memoria è M + R. Supponiamo che StackOverflowError venga lanciato dopo C chiamate ricorsive riuscite, cioè M + C * R <= X e M + C * (R + 1)> X. Al momento del primo StackOverflowError, è rimasta memoria X - M - C * R.

Per poter correre System.out.prinln, abbiamo bisogno di P quantità di spazio rimasto nello stack. Se accade che X - M - C * R> = P, verrà stampato 0. Se P richiede più spazio, rimuoviamo i frame dallo stack, guadagnando memoria R al costo di cnt ++.

Quando printlnè finalmente in grado di funzionare, X - M - (C - cnt) * R> = P. Quindi se P è grande per un particolare sistema, allora cnt sarà grande.

Diamo un'occhiata a questo con alcuni esempi.

Esempio 1: supponi

  • X = 100
  • M = 1
  • R = 2
  • P = 1

Quindi C = floor ((XM) / R) = 49 e cnt = soffitto ((P - (X - M - C * R)) / R) = 0.

Esempio 2: supponi che

  • X = 100
  • M = 1
  • R = 5
  • P = 12

Allora C = 19 e cnt = 2.

Esempio 3: supponiamo che

  • X = 101
  • M = 1
  • R = 5
  • P = 12

Allora C = 20 e cnt = 3.

Esempio 4: supponiamo che

  • X = 101
  • M = 2
  • R = 5
  • P = 12

Allora C = 19 e cnt = 2.

Quindi, vediamo che sia il sistema (M, R e P) che la dimensione dello stack (X) influiscono su cnt.

Come nota a margine, non importa quanto spazio catchrichiede per iniziare. Finché non c'è abbastanza spazio per catch, cnt non aumenterà, quindi non ci sono effetti esterni.

MODIFICARE

Riprendo quello che ho detto catch. Ha un ruolo. Supponiamo che richieda T quantità di spazio per iniziare. cnt inizia ad aumentare quando lo spazio rimanente è maggiore di T e printlnviene eseguito quando lo spazio residuo è maggiore di T + P. Ciò aggiunge un ulteriore passaggio ai calcoli e confonde ulteriormente l'analisi già confusa.

MODIFICARE

Finalmente ho trovato il tempo per eseguire alcuni esperimenti a sostegno della mia teoria. Sfortunatamente, la teoria non sembra corrispondere agli esperimenti. Quello che realmente accade è molto diverso.

Configurazione dell'esperimento: server Ubuntu 12.04 con java predefinito e jdk predefinito. Xss a partire da 70.000 con incrementi di 1 byte fino a 460.000.

I risultati sono disponibili all'indirizzo: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Ho creato un'altra versione in cui ogni punto dati ripetuto viene rimosso. In altre parole, vengono mostrati solo i punti diversi dal precedente. Questo rende più facile vedere le anomalie. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA


Grazie per il buon riepilogo, immagino che tutto si riduca alla domanda: cosa influisce su M, R e P (dato che X può essere impostato dall'opzione VM -Xss)?
flrnb

@flrnb M, R e P sono specifici del sistema. Non puoi cambiarli facilmente. Mi aspetto che differiscano anche tra alcune versioni.
John Tseng

Allora, perché ottengo risultati diversi alterando Xss (aka X)? Cambiare X da 100 a 10000, dato che M, R e P rimangono gli stessi, non dovrebbe influenzare cnt secondo la tua formula, o mi sbaglio?
flrnb

@flrnb X da solo cambia cnt a causa della natura discreta di queste variabili. Gli esempi 2 e 3 differiscono solo su X, ma cnt è diverso.
John Tseng

1
@JohnTseng Considero anche la tua risposta la più comprensibile e completa al momento - in ogni caso, sarei davvero interessato a come appare lo stack nel momento in cui StackOverflowErrorviene lanciato e come questo influisce sull'output. Se conteneva solo un riferimento a uno stack frame nell'heap (come suggerito da Jay), l'output dovrebbe essere abbastanza prevedibile per un dato sistema.
flrnb

20

Questa è la vittima di una cattiva chiamata ricorsiva. Poiché ti stai chiedendo perché il valore di cnt varia, è perché la dimensione dello stack dipende dalla piattaforma. Java SE 6 su Windows ha una dimensione dello stack predefinita di 320k nella VM a 32 bit e 1024k nella VM a 64 bit. Puoi leggere di più qui .

Puoi eseguire utilizzando diverse dimensioni dello stack e vedrai diversi valori di cnt prima che lo stack esca

java -Xss1024k RandomNumberGenerator

Non vedi il valore di cnt stampato più volte anche se il valore è maggiore di 1 a volte perché la tua istruzione print genera anche un errore di cui puoi eseguire il debug per essere sicuro tramite Eclipse o altri IDE.

Se preferisci, puoi modificare il codice come segue per eseguire il debug in base all'esecuzione dell'istruzione:

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

AGGIORNARE:

Poiché ciò sta ottenendo molta più attenzione, facciamo un altro esempio per rendere le cose più chiare:

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

Abbiamo creato un altro metodo chiamato overflow per eseguire una ricorsione errata e rimosso l' istruzione println dal blocco catch in modo che non inizi a generare un altro insieme di errori durante il tentativo di stampa. Funziona come previsto. Puoi provare a mettere System.out.println (cnt); istruzione dopo cnt ++ sopra e compilare. Quindi eseguire più volte. A seconda della piattaforma, potresti ottenere diversi valori di cnt .

Questo è il motivo per cui generalmente non rileviamo errori perché il mistero nel codice non è fantasia.


13

Il comportamento dipende dalla dimensione dello stack (che può essere impostata manualmente utilizzando Xss. La dimensione dello stack è specifica dell'architettura. Dal codice sorgente JDK 7 :

// La dimensione predefinita dello stack su Windows è determinata dall'eseguibile (java.exe
// ha un valore predefinito di 320K / 1MB [32bit / 64bit]). A seconda della versione di Windows, la modifica di
// ThreadStackSize su un valore diverso da zero potrebbe avere un impatto significativo sull'utilizzo della memoria.
// Visualizza i commenti in os_windows.cpp.

Quindi, quando StackOverflowErrorviene lanciato l'errore, l'errore viene catturato nel blocco catch. Ecco println()un'altra chiamata allo stack che genera nuovamente l'eccezione. Questo viene ripetuto.

Quante volte si ripete? - Beh, dipende da quando JVM pensa che non sia più stackoverflow. E questo dipende dalla dimensione dello stack di ciascuna chiamata di funzione (difficile da trovare) e dal file Xss. Come accennato in precedenza, la dimensione totale predefinita e la dimensione di ciascuna chiamata di funzione (dipende dalla dimensione della pagina di memoria, ecc.) È specifica della piattaforma. Da qui un comportamento diverso.

Chiamare la javachiamata con -Xss 4Mmi dà 41. Da qui la correlazione.


4
Non capisco perché la dimensione dello stack dovrebbe influire sul risultato, poiché è già superata quando proviamo a stampare il valore di cnt. Pertanto, l'unica differenza potrebbe derivare dalla "dimensione dello stack di ciascuna chiamata di funzione". E non capisco perché questo dovrebbe variare tra 2 macchine che eseguono la stessa versione JVM.
flrnb

Il comportamento esatto può essere ottenuto solo dall'origine JVM. Ma il motivo potrebbe essere questo. Ricorda anche che catchè un blocco e che occupa la memoria in pila. Quanta memoria occupa ogni chiamata di metodo non può essere conosciuta. Quando la pila viene cancellata, stai aggiungendo un altro blocco di catche così via. Questo potrebbe essere il comportamento. Questa è solo speculazione.
Jatin

E la dimensione dello stack può differire in due macchine diverse. La dimensione dello stack dipende da molti fattori basati sul sistema operativo, ovvero la dimensione della pagina di memoria, ecc.
Jatin

6

Penso che il numero visualizzato sia il numero di volte in cui la System.out.printlnchiamata genera l' Stackoverfloweccezione.

Probabilmente dipende dall'implementazione del printlne dal numero di chiamate di stacking in esso effettuate.

Come un'illustrazione:

La main()chiamata attiva l' Stackoverfloweccezione alla chiamata i. La chiamata i-1 di main cattura l'eccezione e chiama printlnche fa scattare una seconda Stackoverflow. cntottenere incremento a 1. La chiamata i-2 di main cattura ora l'eccezione e chiama println. In printlnun metodo si chiama attivazione di una terza eccezione. cntottenere l'incremento a 2. questo continua fino a quando è printlnpossibile effettuare tutte le chiamate necessarie e infine visualizzare il valore di cnt.

Ciò dipende quindi dall'effettiva implementazione di println.

Per il JDK7 o rileva la chiamata ciclica e genera l'eccezione prima o mantiene alcune risorse dello stack e lancia l'eccezione prima di raggiungere il limite per dare un po 'di spazio alla logica di correzione o l' printlnimplementazione non effettua chiamate o l'operazione ++ viene eseguita dopo la printlnchiamata quindi è bypassata dall'eccezione.


Questo è ciò che intendevo con "Ho pensato che forse fosse perché System.out.println ha bisogno di 3 segmenti nello stack di chiamate" - ma ero perplesso perché è esattamente questo numero e ora sono ancora più perplesso perché il numero differisce così ampiamente tra i diversi macchine (virtuali)
flrnb

In parte sono d'accordo, ma ciò che non sono d'accordo è sull'affermazione "dipendente dall'effettiva implementazione di println". Ha a che fare con la dimensione dello stack in ogni jvm piuttosto che con l'implementazione.
Jatin

6
  1. mainricorre su se stesso fino a quando non supera lo stack alla profondità di ricorsione R.
  2. R-1Viene eseguito il blocco catch alla profondità di ricorsione .
  3. Il blocco catch alla profondità di ricorsione R-1valuta cnt++.
  4. Il blocco catch in profondità R-1chiama println, posizionando cntil vecchio valore in pila. printlnchiamerà internamente altri metodi e usa variabili locali e cose del genere. Tutti questi processi richiedono spazio sullo stack.
  5. Poiché lo stack stava già superando il limite e la chiamata / esecuzione printlnrichiede lo spazio dello stack, viene attivato un nuovo overflow dello stack in profondità R-1anziché in profondità R.
  6. I passaggi 2-5 si ripetono, ma alla profondità della ricorsione R-2.
  7. I passaggi 2-5 si ripetono, ma alla profondità della ricorsione R-3.
  8. I passaggi 2-5 si ripetono, ma alla profondità della ricorsione R-4.
  9. I passaggi 2-4 si ripetono, ma alla profondità della ricorsione R-5.
  10. Accade così che ora ci sia abbastanza spazio nello stack per il printlncompletamento (si noti che questo è un dettaglio di implementazione, potrebbe variare).
  11. cntstato post-incrementato a profondità R-1, R-2, R-3, R-4, e infine aR-5 . Il quinto post-incremento ha restituito quattro, che è ciò che è stato stampato.
  12. Una volta maincompletato con successo in profondità R-5, l'intero stack si svolge senza che vengano eseguiti più blocchi di cattura e il programma viene completato.

1

Dopo aver scavato per un po ', non posso dire di aver trovato la risposta, ma penso che sia abbastanza vicino ora.

Innanzitutto, dobbiamo sapere quando StackOverflowErrorverrà lanciato un testamento. Infatti, lo stack per un thread java memorizza i frame, che contengono tutti i dati necessari per invocare un metodo e riprendere. Secondo le specifiche del linguaggio Java per JAVA 6 , quando si richiama un metodo,

Se non è disponibile memoria sufficiente per creare un frame di attivazione di questo tipo, viene generato uno StackOverflowError.

In secondo luogo, dovremmo chiarire cosa significa " non è disponibile memoria sufficiente per creare un tale frame di attivazione ". Secondo le specifiche della Java Virtual Machine per JAVA 6 ,

i frame possono essere allocati nell'heap.

Quindi, quando viene creato un frame, dovrebbe esserci spazio sufficiente per creare uno stack frame e spazio sufficiente per memorizzare il nuovo riferimento che punta al nuovo stack frame se il frame è allocato nell'heap.

Ora torniamo alla domanda. Da quanto sopra, possiamo sapere che quando un metodo viene eseguito, potrebbe costare la stessa quantità di spazio dello stack. E invocare System.out.println(può) richiede 5 livelli di invocazione del metodo, quindi è necessario creare 5 frame. Quindi, quando StackOverflowErrorviene eliminato, deve tornare indietro di 5 volte per ottenere abbastanza spazio nello stack per memorizzare i riferimenti di 5 frame. Quindi 4 viene stampato. Perché non 5? Perché usi cnt++. Modificalo in ++cnt, quindi otterrai 5.

E noterai che quando la dimensione dello stack sale a un livello elevato, a volte ne otterrai 50. Questo perché la quantità di spazio disponibile nell'heap deve essere presa in considerazione. Quando la dimensione dello stack è troppo grande, forse lo spazio dell'heap si esaurirà prima dello stack. E (forse) la dimensione effettiva degli stack frame System.out.printlnè di circa 51 volte main, quindi torna indietro di 51 volte e ne stampa 50.


Il mio primo pensiero è stato anche contare i livelli di invocazioni di metodi (e hai ragione, non ho prestato attenzione al fatto che inserisco increment cnt), ma se la soluzione fosse così semplice, perché i risultati dovrebbero variare così tanto tra le piattaforme e implementazioni VM?
flrnb

@flrnb Questo perché diverse piattaforme possono influenzare la dimensione dello stack frame e diverse versioni di jre influenzeranno l'implementazione System.out.printo la strategia di esecuzione del metodo. Come descritto in precedenza, l'implementazione della VM influisce anche sulla posizione in cui verrà archiviato lo stack frame.
Jay

0

Questa non è esattamente una risposta alla domanda, ma volevo solo aggiungere qualcosa alla domanda originale in cui mi sono imbattuto e come ho capito il problema:

Nel problema originale l'eccezione viene rilevata dove era possibile:

Ad esempio, con jdk 1.7 viene catturato al primo posto in cui si verifica.

ma nelle versioni precedenti di jdk sembra che l'eccezione non venga catturata al primo posto, quindi 4, 50 ecc.

Ora se rimuovi il blocco try catch come segue

public static void main( String[] args ){
    System.out.println(cnt++);
    main(args);
}

Quindi vedrai tutti i valori delle cnteccezioni lanciate (su jdk 1.7).

Ho usato netbeans per vedere l'output, poiché cmd non mostrerà tutto l'output e l'eccezione generata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.