Come aumentare la dimensione dello stack Java?

Ho fatto questa domanda per sapere come aumentare la dimensione dello stack delle chiamate in runtime nella JVM. Ho una risposta a questo, e ho anche ricevuto molte risposte e commenti utili su come Java gestisce la situazione in cui è necessario un grande stack di runtime. Ho esteso la mia domanda con il riepilogo delle risposte.

Inizialmente volevo aumentare le dimensioni dello stack JVM in modo da programmi come le esecuzioni senza StackOverflowError .

 public class TT { public static long fact(int n) { return n < 2 ? 1 : n * fact(n - 1); } public static void main(String[] args) { System.out.println(fact(1 << 15)); } } 

L’impostazione di configurazione corrispondente è il java -Xss... riga di comando java -Xss... con un valore abbastanza grande. Per il programma TT sopra, funziona così con la JVM di OpenJDK:

 $ javac TT.java $ java -Xss4m TT 

Una delle risposte ha anche evidenziato che i flag -X... sono dipendenti dall’implementazione. Stavo usando

 java version "1.6.0_18" OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1~8.04.3) OpenJDK 64-Bit Server VM (build 16.0-b13, mixed mode) 

È anche ansible specificare una grande pila solo per un thread (vedere in una delle risposte come). Questo è raccomandato su java -Xss... per evitare di sprecare memoria per i thread che non ne hanno bisogno.

Ero curioso di sapere quanto grande fosse esattamente il programma sopra, quindi l’ho eseguito n aumentato:

  • -Xss4m può essere sufficiente per fact(1 << 15)
  • -Xss5m può essere sufficiente per fact(1 << 17)
  • -Xss7m può essere sufficiente per fact(1 << 18)
  • -Xss9m può essere abbastanza per fact(1 << 19)
  • -Xss18m può essere sufficiente per fact(1 << 20)
  • -Xss35m può essere abbastanza per fact(1 << 21)
  • -Xss68m può essere sufficiente per fact(1 << 22)
  • -Xss129m può essere sufficiente per fact(1 << 23)
  • -Xss258m può essere sufficiente per fact(1 << 24)
  • -Xss515m può essere sufficiente per fact(1 << 25)

Dai numeri precedenti sembra che Java stia usando circa 16 byte per stack frame per la funzione sopra, il che è ragionevole.

L’enumerazione di cui sopra può essere sufficiente invece di essere sufficiente , perché il requisito di stack non è deterministico: eseguirlo più volte con lo stesso file sorgente e lo stesso -Xss... volte riesce e talvolta genera un StackOverflowError . Ad es. Per 1 << 20, -Xss18m era sufficiente in 7 run su 10, e -Xss19m non era sempre abbastanza, ma -Xss20m era abbastanza (in tutte le 100 -Xss20m su 100). La garbage collection, il JIT che si avvia o qualcos’altro causa questo comportamento non deterministico?

La traccia dello stack stampata su StackOverflowError (e probabilmente anche su altre eccezioni) mostra solo gli ultimi 1024 elementi dello stack di runtime. Una risposta qui sotto mostra come contare l’esatta profondità raggiunta (che potrebbe essere molto più grande di 1024).

Molte persone che hanno risposto hanno sottolineato che è una buona e sicura pratica di codifica considerare implementazioni alternative e meno affamate dello stesso algoritmo. In generale, è ansible convertire un insieme di funzioni ricorsive in funzioni iterative (usando un object Stack , ad esempio, che viene popolato nell’heap anziché nello stack di runtime). Per questa particolare funzione dei fact , è abbastanza facile convertirlo. La mia versione iterativa sarebbe simile a:

 public class TTIterative { public static long fact(int n) { if (n  65) return 0; // Enough powers of 2 in the product to make it (long)0. long f = 2; for (int i = 3; i <= n; ++i) { f *= i; } return f; } public static void main(String[] args) { System.out.println(fact(1 << 15)); } } 

Per vostra informazione, come dimostra la soluzione iterativa sopra riportata, la funzione fact non può calcolare il fattoriale esatto dei numeri sopra i 65 (in realtà, anche sopra i 20), perché il tipo di Java built-in long traboccherebbe. Il fact refactoring quindi restituirebbe un BigInteger invece di long fornirebbe risultati esatti anche per i grandi input.

Hmm … funziona per me e con meno di 999 MB di stack:

 > java -Xss4m Test 0 

(Windows JDK 7, build VM client 17.0-b05 e Linux JDK 6 – stesse informazioni sulla versione che hai pubblicato)

Immagino tu abbia calcolato la “profondità di 1024” dalle linee ricorrenti nella traccia dello stack?

Ovviamente, la lunghezza dell’array di traccia stack in Throwable sembra essere limitata a 1024. Prova il seguente programma:

 public class Test { public static void main(String[] args) { try { System.out.println(fact(1 << 15)); } catch (StackOverflowError e) { System.err.println("true recursion level was " + level); System.err.println("reported recursion level was " + e.getStackTrace().length); } } private static int level = 0; public static long fact(int n) { level++; return n < 2 ? n : n * fact(n - 1); } } 

Se vuoi giocare con le dimensioni dello stack di thread, ti consigliamo di guardare l’opzione -Xss su JVM Hotspot. Potrebbe essere qualcosa di diverso su macchine virtuali non Hotspot poiché i parametri -X della JVM sono specifici della distribuzione, IIRC.

Su Hotspot, sembra java -Xss16M se si desidera rendere la dimensione di 16 mega.

Digita java -X -help se vuoi vedere tutti i parametri JVM specifici della distribuzione che puoi inserire. Non sono sicuro che funzioni sugli stessi JVM, ma stampa tutti i parametri specifici di Hotspot.

Per quello che vale, ti consiglio di limitare l’utilizzo dei metodi ricorsivi in ​​Java. Non è troppo bello per ottimizzarle – per esempio la JVM non supporta la ricorsione in coda (si veda JVM impedisce le ottimizzazioni delle chiamate tail? ). Prova a rifare il codice fattoriale qui sopra per utilizzare un ciclo while anziché le chiamate al metodo ricorsivo.

L’unico modo per controllare la dimensione dello stack nel processo è iniziare una nuova Thread . Ma puoi anche controllare creando un processo sub-Java -Xss con il parametro -Xss .

 public class TT { private static int level = 0; public static long fact(int n) { level++; return n < 2 ? n : n * fact(n - 1); } public static void main(String[] args) throws InterruptedException { Thread t = new Thread(null, null, "TT", 1000000) { @Override public void run() { try { level = 0; System.out.println(fact(1 << 15)); } catch (StackOverflowError e) { System.err.println("true recursion level was " + level); System.err.println("reported recursion level was " + e.getStackTrace().length); } } }; t.start(); t.join(); try { level = 0; System.out.println(fact(1 << 15)); } catch (StackOverflowError e) { System.err.println("true recursion level was " + level); System.err.println("reported recursion level was " + e.getStackTrace().length); } } } 

Aggiungi questa opzione

 --driver-java-options -Xss512m 

al tuo comando spark-submit risolverà questo problema.

È difficile dare una soluzione sensata poiché si desidera evitare tutti gli approcci sensati. Il refactoring di una riga di codice è la soluzione definitiva.

Nota: l’utilizzo di -Xss imposta la dimensione dello stack di ogni thread ed è una pessima idea.

Un altro approccio è la manipolazione del codice byte per modificare il codice come segue;

 public static long fact(int n) { return n < 2 ? n : n > 127 ? 0 : n * fact(n - 1); } 

dato ogni risposta per n> 127 è 0. Questo evita di cambiare il codice sorgente.

Strano! Stai dicendo che vuoi generare una ricorsione di 1 << 15 di profondità ??? !!!!

Suggerirei NON provarlo. La dimensione della pila sarà 2^15 * sizeof(stack-frame) . Non so quale sia la dimensione dello stack frame, ma 2 ^ 15 è 32.768. Praticamente … Beh, se si ferma a 1024 (2 ^ 10) dovrai renderlo 2 ^ 5 volte più grande, è 32 volte più grande della tua impostazione attuale.

Altri poster hanno indicato come aumentare la memoria e che è ansible memoizzare le chiamate. Suggerirei che per molte applicazioni, è ansible utilizzare la formula di Stirling per approssimare grande n! molto rapidamente con quasi nessuna impronta di memoria.

Date un’occhiata a questo post, che ha qualche analisi della funzione e del codice:

http://threebrothers.org/brendan/blog/stirlings-approximation-formula-clojure/

Ho fatto l’ anagramma excersize , che è come il problema di Count Change ma con 50.000 denominazioni (monete). Non sono sicuro che possa essere fatto in modo iterativo , non mi interessa. So solo che l’opzione -xss non ha avuto alcun effetto – Ho sempre fallito dopo 1024 frame di stack (potrebbe essere scala fa un brutto lavoro di consegna fino alla limitazione java o printStackTrace.) Non lo so). Questa è una ctriggers opzione, come spiegato comunque. Non vuoi che tutti i thread in app siano mostruosi. Tuttavia, ho fatto alcuni esperimenti con il nuovo Thread (stack size). Questo funziona davvero,

  def measureStackDepth(ss: Long): Long = { var depth: Long = 0 val thread: Thread = new Thread(null, new Runnable() { override def run() { try { def sum(n: Long): Long = {depth += 1; if (n== 0) 0 else sum(n-1) + 1} println("fact = " + sum(ss * 10)) } catch { case e: StackOverflowError => // eat the exception, that is expected } } }, "deep stack for money exchange", ss) thread.start() thread.join() depth } //> measureStackDepth: (ss: Long)Long for (ss <- (0 to 10)) println("ss = 10^" + ss + " allows stack of size " -> measureStackDepth((scala.math.pow (10, ss)).toLong) ) //> fact = 10 //| (ss = 10^0 allows stack of size ,11) //| fact = 100 //| (ss = 10^1 allows stack of size ,101) //| fact = 1000 //| (ss = 10^2 allows stack of size ,1001) //| fact = 10000 //| (ss = 10^3 allows stack of size ,10001) //| (ss = 10^4 allows stack of size ,1336) //| (ss = 10^5 allows stack of size ,5456) //| (ss = 10^6 allows stack of size ,62736) //| (ss = 10^7 allows stack of size ,623876) //| (ss = 10^8 allows stack of size ,6247732) //| (ss = 10^9 allows stack of size ,62498160) 

Vedete che lo stack può crescere esponenzialmente più in profondità con esponenzialmente più stack assegnato al thread.