Me preguntaba qué sucede cuando intentas detectar un StackOverflowError y se te ocurrió el siguiente método:
class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
Ahora mi pregunta:
¿Por qué este método imprime '4'?
Pensé que tal vez era porque System.out.println()
necesita 3 segmentos en la pila de llamadas, pero no sé de dónde viene el número 3. Cuando observa el código fuente (y el código de bytes) de System.out.println()
, normalmente daría lugar a muchas más invocaciones de métodos que 3 (por lo que 3 segmentos en la pila de llamadas no serían suficientes). Si es debido a las optimizaciones que aplica la máquina virtual de Hotspot (inserción de métodos), me pregunto si el resultado sería diferente en otra máquina virtual.
Editar :
Como la salida parece ser muy específica de JVM, obtengo el resultado 4 usando
Java (TM) SE Runtime Environment (compilación 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit Server VM (compilación 20.14-b01, modo mixto)
Explicación por qué creo que esta pregunta es diferente de Comprender la pila de Java :
Mi pregunta no es por qué hay un cnt> 0 (obviamente porque System.out.println()
requiere un tamaño de pila y arroja otro StackOverflowError
antes de que se imprima algo), sino por qué tiene el valor particular de 4, respectivamente 0,3,8,55 o algo más en otro sistemas.
fuente
5
,6
y38
con Java 1.7.0_10Respuestas:
Creo que los demás han hecho un buen trabajo al explicar por qué cnt> 0, pero no hay suficientes detalles sobre por qué cnt = 4 y por qué cnt varía tanto entre diferentes configuraciones. Intentaré llenar ese vacío aquí.
Dejar
System.out.println
Cuando entramos por primera vez en main, el espacio que queda es XM. Cada llamada recursiva ocupa R más memoria. Entonces, para 1 llamada recursiva (1 más que la original), el uso de memoria es M + R. Suponga que StackOverflowError se lanza después de C llamadas recursivas exitosas, es decir, M + C * R <= X y M + C * (R + 1)> X. En el momento del primer StackOverflowError, queda memoria X - M - C * R.
Para poder correr
System.out.prinln
, necesitamos P cantidad de espacio que queda en la pila. Si sucede que X - M - C * R> = P, se imprimirá 0. Si P requiere más espacio, entonces eliminamos marcos de la pila, ganando memoria R a costa de cnt ++.Cuando
println
finalmente pueda ejecutarse, X - M - (C - cnt) * R> = P. Entonces, si P es grande para un sistema en particular, entonces cnt será grande.Veamos esto con algunos ejemplos.
Ejemplo 1: suponga
Entonces C = piso ((XM) / R) = 49, y cnt = techo ((P - (X - M - C * R)) / R) = 0.
Ejemplo 2: suponga que
Entonces C = 19 y cnt = 2.
Ejemplo 3: suponga que
Entonces C = 20 y cnt = 3.
Ejemplo 4: suponga que
Entonces C = 19 y cnt = 2.
Por tanto, vemos que tanto el sistema (M, R y P) como el tamaño de la pila (X) afectan a cnt.
Como nota al margen, no importa cuánto espacio se
catch
requiera para comenzar. Mientras no haya suficiente espacio paracatch
, cnt no aumentará, por lo que no habrá efectos externos.EDITAR
Retiro lo que dije
catch
. Juega un papel. Suponga que requiere T cantidad de espacio para comenzar. cnt comienza a incrementarse cuando el espacio sobrante es mayor que T, yprintln
ejecuta cuando el espacio sobrante es mayor que T + P. Esto agrega un paso adicional a los cálculos y enturbia aún más el análisis ya confuso.EDITAR
Finalmente encontré tiempo para realizar algunos experimentos para respaldar mi teoría. Desafortunadamente, la teoría no parece coincidir con los experimentos. Lo que realmente sucede es muy diferente.
Configuración del experimento: servidor Ubuntu 12.04 con java predeterminado y jdk predeterminado. Xss a partir de 70.000 en incrementos de 1 byte hasta 460.000.
Los resultados están disponibles en: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM He creado otra versión en la que se eliminan todos los puntos de datos repetidos. En otras palabras, solo se muestran los puntos que son diferentes a los anteriores. Esto facilita la visualización de anomalías. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
fuente
StackOverflowError
se lanza y cómo esto afecta la salida. Si solo contiene una referencia a un marco de pila en el montón (como sugirió Jay), entonces la salida debería ser bastante predecible para un sistema dado.Esta es la víctima de una mala llamada recursiva. Como se pregunta por qué varía el valor de cnt , es porque el tamaño de la pila depende de la plataforma. Java SE 6 en Windows tiene un tamaño de pila predeterminado de 320k en la máquina virtual de 32 bits y 1024k en la máquina virtual de 64 bits. Puedes leer más aquí .
Puede ejecutar usando diferentes tamaños de pila y verá diferentes valores de cnt antes de que la pila se desborde.
No ve que el valor de cnt se imprima varias veces, aunque el valor es mayor que 1 a veces porque su declaración de impresión también arroja un error que puede depurar para estar seguro a través de Eclipse u otros IDE.
Puede cambiar el código a lo siguiente para depurar por ejecución de declaración si lo prefiere:
ACTUALIZAR:
Como esto recibe mucha más atención, veamos otro ejemplo para aclarar las cosas.
Creamos otro método llamado overflow para hacer una recursión incorrecta y eliminamos la declaración println del bloque catch para que no comience a arrojar otro conjunto de errores al intentar imprimir. Esto funciona como se esperaba. Puede intentar poner System.out.println (cnt); declaración después de cnt ++ anterior y compilar. Luego, ejecute varias veces. Dependiendo de su plataforma, puede obtener diferentes valores de cnt .
Es por eso que generalmente no detectamos errores porque el misterio en el código no es una fantasía.
fuente
El comportamiento depende del tamaño de la pila (que se puede configurar manualmente usando
Xss
. El tamaño de la pila es específico de la arquitectura. Del código fuente de JDK 7 :Entonces, cuando
StackOverflowError
se lanza, el error se detecta en el bloque catch. Aquíprintln()
hay otra llamada de pila que arroja una excepción nuevamente. Esto se repite.¿Cuántas veces se repite? - Bueno, depende de cuándo JVM crea que ya no es stackoverflow. Y eso depende del tamaño de la pila de cada llamada de función (difícil de encontrar) y del
Xss
. Como se mencionó anteriormente, el tamaño total predeterminado y el tamaño de cada llamada de función (depende del tamaño de la página de memoria, etc.) es específico de la plataforma. De ahí un comportamiento diferente.Llamar a la
java
llamada con-Xss 4M
me da41
. De ahí la correlación.fuente
catch
es un bloque y eso ocupa memoria en la pila. No se puede saber cuánta memoria toma cada llamada de método. Cuando la pila se borra, está agregando un bloque más decatch
y así. Este podría ser el comportamiento. Esto es solo especulación.Creo que el número que se muestra es el número de veces que la
System.out.println
llamada lanza laStackoverflow
excepción.Probablemente dependa de la implementación del
println
y del número de llamadas de apilamiento que se realice en él.Como una ilustracion:
La
main()
llamada activa laStackoverflow
excepción en la llamada i. La llamada i-1 de main captura la excepción y llamaprintln
que activa un segundoStackoverflow
.cnt
obtener incremento a 1. La llamada i-2 de main captura ahora la excepción y la llamadaprintln
. Enprintln
un método se llama desencadenar una tercera excepción.cnt
obtener el incremento a 2. esto continúa hasta queprintln
pueda realizar todas las llamadas necesarias y finalmente mostrar el valor decnt
.Esto depende entonces de la implementación real de
println
.Para el JDK7, detecta la llamada cíclica y lanza la excepción antes, o mantiene algún recurso de pila y lanza la excepción antes de alcanzar el límite para dar espacio para la lógica de corrección o la
println
implementación no realiza llamadas o bien la operación ++ se realiza después laprintln
llamada, por tanto, pasa por alto la excepción.fuente
main
se repite sobre sí mismo hasta que desborda la pila en profundidad de recursividadR
.R-1
ejecuta el bloque de captura a la profundidad de recursividad .R-1
evalúa el bloque de captura en la profundidad de recursióncnt++
.R-1
llamadas de profundidadprintln
, colocandocnt
el valor anterior en la pila.println
llamará internamente a otros métodos y utilizará variables y cosas locales. Todos estos procesos requieren espacio de pila.println
requiere espacio en la pila, se activa un nuevo desbordamiento de pila en profundidad enR-1
lugar de profundidadR
.R-2
.R-3
.R-4
.R-5
.println
completar (tenga en cuenta que este es un detalle de implementación, puede variar).cnt
fue post-incrementado a profundidadesR-1
,R-2
,R-3
,R-4
, y finalmente aR-5
. El quinto incremento posterior devolvió cuatro, que es lo que se imprimió.main
completado con éxito en profundidadR-5
, toda la pila se desenrolla sin que se ejecuten más bloques de captura y el programa se completa.fuente
Después de investigar un poco, no puedo decir que encontré la respuesta, pero creo que ahora está bastante cerca.
Primero, necesitamos saber cuándo se
StackOverflowError
lanzará un will. De hecho, la pila de un hilo de Java almacena marcos, que contienen todos los datos necesarios para invocar un método y reanudar. Según las Especificaciones del lenguaje Java para JAVA 6 , al invocar un método,En segundo lugar, debemos dejar en claro qué es " no hay suficiente memoria disponible para crear un marco de activación de este tipo ". Según las especificaciones de la máquina virtual Java para JAVA 6 ,
Por lo tanto, cuando se crea un marco, debe haber suficiente espacio de pila para crear un marco de pila y suficiente espacio de pila para almacenar la nueva referencia que apunta al nuevo marco de pila si el marco está asignado a pila.
Ahora volvamos a la pregunta. Por lo anterior, podemos saber que cuando se ejecuta un método, puede costar la misma cantidad de espacio de pila. Y la invocación
System.out.println
(may) necesita 5 niveles de invocación de método, por lo que es necesario crear 5 marcos. Luego, cuandoStackOverflowError
se descarta, tiene que retroceder 5 veces para obtener suficiente espacio de pila para almacenar referencias de 5 marcos. Por lo tanto, se imprime 4. ¿Por qué no 5? Porque usascnt++
. Cámbielo a++cnt
y obtendrá 5.Y notará que cuando el tamaño de la pila llega a un nivel alto, obtendrá 50 a veces. Esto se debe a que en ese momento debe tenerse en cuenta la cantidad de espacio de pila disponible. Cuando el tamaño de la pila es demasiado grande, es posible que el espacio de pila se agote antes que la pila. Y (tal vez) el tamaño real de los marcos de pila de
System.out.println
es aproximadamente 51 vecesmain
, por lo que retrocede 51 veces e imprime 50.fuente
System.out.print
o la estrategia de ejecución del método. Como se describió anteriormente, la implementación de la VM también afecta dónde se almacenará realmente el marco de la pila.Esta no es exactamente una respuesta a la pregunta, pero solo quería agregar algo a la pregunta original que encontré y cómo entendí el problema:
En el problema original, la excepción se detecta donde era posible:
Por ejemplo, con jdk 1.7 se detecta en el primer lugar de ocurrencia.
pero en versiones anteriores de jdk parece que la excepción no se detecta en el primer lugar de ocurrencia, por lo tanto, 4, 50, etc.
Ahora, si elimina el bloque try catch de la siguiente manera
Luego verá todos los valores de
cnt
las excepciones lanzadas (en jdk 1.7).Usé netbeans para ver la salida, ya que el cmd no mostrará toda la salida y la excepción lanzada.
fuente