¿Cómo aumentar el tamaño de la pila de Java?
Hice esta pregunta para saber cómo aumentar el tamaño de la pila de llamadas en tiempo de ejecución en la JVM. Tengo una respuesta a esto, y también tengo muchas respuestas útiles y comentarios relevantes sobre cómo Java maneja la situación en la que se necesita una gran pila de tiempo de ejecución. He ampliado mi pregunta con el resumen de las respuestas.
Originalmente quería aumentar el tamaño de la pila JVM para que programas como se ejecuten sin un archivo 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));
}
}
El valor de configuración correspondiente es el java -Xss...
indicador de línea de comando con un valor suficientemente grande. Para el programa TT
anterior, funciona así con la JVM de OpenJDK:
$ javac TT.java
$ java -Xss4m TT
Una de las respuestas también señaló que las -X...
banderas dependen de la implementación. yo estaba 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)
También es posible especificar una pila grande solo para un hilo (vea cómo hacerlo en una de las respuestas). Se recomienda esto java -Xss...
para evitar desperdiciar memoria en subprocesos que no la necesitan.
Tenía curiosidad por saber qué tamaño de pila necesita exactamente el programa anterior, así que lo ejecuté n
con mayor tamaño:
- -Xss4m puede ser suficiente para
fact(1 << 15)
- -Xss5m puede ser suficiente para
fact(1 << 17)
- -Xss7m puede ser suficiente para
fact(1 << 18)
- -Xss9m puede ser suficiente para
fact(1 << 19)
- -Xss18m puede ser suficiente para
fact(1 << 20)
- -Xss35m puede ser suficiente para
fact(1 << 21)
- -Xss68m puede ser suficiente para
fact(1 << 22)
- -Xss129m puede ser suficiente para
fact(1 << 23)
- -Xss258m puede ser suficiente para
fact(1 << 24)
- -Xss515m puede ser suficiente para
fact(1 << 25)
De los números anteriores parece que Java está usando aproximadamente 16 bytes por marco de pila para la función anterior, lo cual es razonable.
La enumeración anterior contiene puede ser suficiente en lugar de es suficiente , porque el requisito de la pila no es determinista: ejecutarlo varias veces con el mismo archivo fuente y el mismo -Xss...
a veces tiene éxito y otras veces produce un archivo StackOverflowError
. Por ejemplo, para 1 << 20, -Xss18m
fue suficiente en 7 ejecuciones de 10, y -Xss19m
tampoco siempre fue suficiente, pero -Xss20m
fue suficiente (en todas 100 ejecuciones de 100). ¿La recolección de basura, el JIT u otra cosa causan este comportamiento no determinista?
El seguimiento de la pila impreso en a StackOverflowError
(y posiblemente también en otras excepciones) muestra solo los 1024 elementos más recientes de la pila en tiempo de ejecución. Una respuesta a continuación demuestra cómo contar la profundidad exacta alcanzada (que podría ser mucho mayor que 1024).
Muchas personas que respondieron señalaron que es una práctica de codificación buena y segura considerar implementaciones alternativas del mismo algoritmo que requieren menos pila. En general, es posible convertir un conjunto de funciones recursivas en funciones iterativas (usando, por ejemplo, un Stack
objeto, que se completa en el montón en lugar de en la pila de tiempo de ejecución). Para esta fact
función en particular, es bastante fácil convertirla. Mi versión iterativa se vería así:
public class TTIterative {
public static long fact(int n) {
if (n < 2) return 1;
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));
}
}
Para su información, como lo muestra la solución iterativa anterior, la fact
función no puede calcular el factorial exacto de números superiores a 65 (en realidad, incluso superiores a 20), porque el tipo integrado de Java long
se desbordaría. Refactorizar fact
para que devuelva a BigInteger
en lugar de long
también produciría resultados exactos para entradas grandes.
Hmm... funciona para mí y con mucho menos de 999 MB de pila:
> java -Xss4m Test
0
(Windows JDK 7, compilación 17.0-b05 de la máquina virtual cliente y Linux JDK 6: la misma información de versión que publicó)
¿Supongo que calculó la "profundidad de 1024" mediante las líneas recurrentes en el seguimiento de la pila?
Obviamente, la longitud de la matriz de seguimiento de la pila en Throwable parece estar limitada a 1024. Pruebe el siguiente programa:
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);
}
}
Si quieres jugar con el tamaño de la pila de subprocesos, querrás mirar la opción -Xss en Hotspot JVM. Puede ser algo diferente en máquinas virtuales que no son Hotspot, ya que los parámetros -X de la JVM son específicos de la distribución, IIRC.
En Hotspot, esto se ve así java -Xss16M
si quieres hacer el tamaño de 16 megas.
Escriba java -X -help
si desea ver todos los parámetros de JVM específicos de la distribución que puede pasar. No estoy seguro de si esto funciona igual en otras JVM, pero imprime todos los parámetros específicos de Hotspot.
Por si sirve de algo, recomendaría limitar el uso de métodos recursivos en Java. No es muy bueno para optimizarlos; por un lado, la JVM no admite la recursividad de cola (consulte ¿La JVM impide las optimizaciones de llamadas de cola? ). Intente refactorizar su código factorial anterior para usar un bucle while en lugar de llamadas a métodos recursivos.
La única forma de controlar el tamaño de la pila dentro del proceso es iniciar uno nuevo Thread
. Pero también puedes controlar creando un subproceso Java autollamado con el -Xss
parámetro.
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);
}
}
}
Es difícil dar una solución sensata ya que se desea evitar todos los enfoques sensatos. Refactorizar una línea de código es la solución sensata.
Nota: Usar -Xss establece el tamaño de la pila de cada hilo y es una muy mala idea.
Otro enfoque es la manipulación del código de bytes para cambiar el código de la siguiente manera;
public static long fact(int n) {
return n < 2 ? n : n > 127 ? 0 : n * fact(n - 1);
}
dada cada respuesta para n > 127 es 0. Esto evita cambiar el código fuente.