paso - java swing tutorial pdf español
¿Por qué este método imprime 4? (7)
Me preguntaba qué pasaría si intentas atrapar un StackOverflowError y obtendrías 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 mira el código fuente (y el código de bytes) de System.out.println()
, normalmente llevaría a más invocaciones de métodos que 3 (por lo que 3 segmentos en la pila de llamadas no serían suficientes). Si es por las optimizaciones que aplica la VM de Hotspot (método en línea), me pregunto si el resultado sería diferente en otra máquina virtual.
Editar :
Como el resultado parece ser altamente específico 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 de por qué creo que esta pregunta es diferente de Comprender la pila de Java :
Mi pregunta no es sobre por qué hay un cnt> 0 (obviamente porque System.out.println()
requiere un tamaño de pila y arroja otro StackOverflowError
antes de que algo se imprima), pero por qué tiene el valor particular de 4, respectivamente 0,3, 8,55 o algo más en otros sistemas.
-
main
recurre en sí mismo hasta que desborda la pila a la profundidad de recursiónR
- Se ejecuta el bloque catch en la profundidad de recursión
R-1
. - El bloque catch en la profundidad de recursión
R-1
evalúacnt++
. - El bloque catch en la profundidad
R-1
llama aprintln
, colocando el antiguo valor decnt
en la pila.println
llamará internamente a otros métodos y usará variables y cosas locales. Todos estos procesos requieren espacio de pila. - Debido a que la pila ya estaba en el límite, y la invocación / ejecución de
println
requiere espacio en la pila, se desencadena un nuevo desbordamiento de pila a profundidadR-1
lugar de profundidadR
- Los pasos 2-5 vuelven a ocurrir, pero a la profundidad de recursión
R-2
. - Los pasos 2-5 vuelven a ocurrir, pero a la profundidad de recursión
R-3
. - Los pasos 2-5 vuelven a ocurrir, pero a la profundidad de recursión
R-4
. - Los pasos 2-4 vuelven a ocurrir, pero a la profundidad de recursión
R-5
. - Ocurre que hay suficiente espacio de pila ahora para que se complete la
println
(tenga en cuenta que este es un detalle de implementación, puede variar). -
cnt
fue post-incrementado en las profundidadesR-1
,R-2
,R-3
,R-4
, y finalmente enR-5
. El quinto incremento posterior devolvió cuatro, que es lo que se imprimió. - Con el material
main
completado con éxito en profundidadR-5
, toda la pila se desenrolla sin que se ejecuten más bloques de captura y el programa finaliza.
Creo que el número que se muestra es el número de veces que la llamada a System.out.println
arroja la excepción .
Probablemente dependa de la implementación de la println
y del número de llamadas de apilamiento que se realizan en ella.
Como una ilustracion:
La llamada main()
excepción
en call i. La llamada i-1 de main captura la excepción y llama a
println
que println
un segundo .
cnt
obtener incremento a 1. La llamada i-2 de catch principal ahora es la excepción y call println
. En println
un método se llama desencadenar una tercera excepción. cnt
obtener incremento a 2. esto continuará hasta que println
pueda hacer todas las llamadas necesarias y finalmente mostrar el valor de cnt
.
Esto depende de la implementación real de println
.
Para el JDK7, o bien detecta la llamada cíclica y arroja la excepción antes, o bien conserva algún recurso de pila y lanza la excepción antes de alcanzar el límite para dejar espacio para la lógica de println
ya sea que la implementación println
no realice llamadas, ya sea que la operación ++ haya finalizado después de la llamada println
es pasar por la excepción.
Creo que los otros 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 mucho entre diferentes configuraciones. Intentaré llenar ese vacío aquí.
Dejar
- X sea el tamaño total de la pila
- M sea el espacio de pila utilizado cuando ingresamos a la línea principal la primera vez
- R sea el espacio de pila aumentar cada vez que entramos en principal
- P sea el espacio de pila necesario para ejecutar
System.out.println
Cuando entramos por primera vez a main, el espacio que queda es XM. Cada llamada recursiva ocupa R más memoria. Entonces, para 1 llamada recursiva (1 más que el original), el uso de la memoria es M + R. Supongamos que Error 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 Error, queda memoria X - M - C * R.
Para poder ejecutar System.out.prinln
, necesitamos P cantidad de espacio restante en la pila. Si sucede que X - M - C * R> = P, entonces se imprimirá 0. Si P requiere más espacio, entonces eliminamos los marcos de la pila, ganando la memoria R a costa de cnt ++.
Cuando println
finalmente puede ejecutarse, X - M - (C - cnt) * R> = P. Entonces, si P es grande para un sistema particular, entonces cnt será grande.
Veamos esto con algunos ejemplos.
Ejemplo 1: supongamos
- X = 100
- M = 1
- R = 2
- P = 1
Entonces C = piso ((XM) / R) = 49, y cnt = techo ((P - (X - M - C * R)) / R) = 0.
Ejemplo 2: supongamos que
- X = 100
- M = 1
- R = 5
- P = 12
Entonces C = 19 y cnt = 2.
Ejemplo 3: supongamos que
- X = 101
- M = 1
- R = 5
- P = 12
Entonces C = 20 y cnt = 3.
Ejemplo 4: supongamos que
- X = 101
- M = 2
- R = 5
- P = 12
Entonces C = 19 y cnt = 2.
Por lo tanto, vemos que tanto el sistema (M, R y P) como el tamaño de pila (X) afectan cnt.
Como nota al margen, no importa cuánto espacio de catch
requiere para comenzar. Mientras no haya suficiente espacio para catch
, entonces cnt no aumentará, por lo que no hay efectos externos.
EDITAR
Retiro lo que dije sobre la catch
. Juega un papel. Supongamos que requiere T cantidad de espacio para comenzar. cnt comienza a incrementarse cuando el espacio sobrante es mayor que T, y println
ejecuta cuando el espacio sobrante es mayor que T + P. Esto agrega un paso adicional a los cálculos y enturbia el análisis ya embarrado.
EDITAR
Finalmente encontré tiempo para ejecutar 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 comenzando en 70,000 en incrementos de 1 byte a 460,000.
Los resultados están disponibles en: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM He creado otra versión donde se eliminan todos los puntos de datos repetidos. En otras palabras, solo se muestran los puntos que son diferentes de los anteriores. Esto hace que sea más fácil ver anomalías. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
Después de cavar por un tiempo, no puedo decir que encuentre la respuesta, pero creo que ya está bastante cerca.
Primero, necesitamos saber cuándo se lanzará Error
. De hecho, la pila para 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,
Si no hay suficiente memoria disponible para crear dicho marco de activación, se lanza un Error.
En segundo lugar, debemos aclarar qué es " no hay suficiente memoria disponible para crear dicho marco de activación ". De acuerdo con Java Virtual Machine Specifications for JAVA 6 ,
los marcos pueden ser asignados
Por lo tanto, cuando se crea un marco, debe haber suficiente espacio en el montón para crear un marco de pila y suficiente espacio de pila para almacenar la nueva referencia que apunte al nuevo marco de pila si el marco está asignado al montón.
Ahora regresemos a la pregunta. De lo anterior, podemos saber que cuando se ejecuta un método, simplemente puede costar la misma cantidad de espacio de pila. Y al invocar System.out.println
(mayo) necesita 5 niveles de invocación de método, por lo que es necesario crear 5 marcos. Luego, cuando se lanza Error
, tiene que retroceder 5 veces para obtener suficiente espacio de pila para almacenar referencias de 5 cuadros. Por lo tanto, 4 se imprime. ¿Por qué no 5? Porque usas cnt++
. Cámbielo a ++cnt
, y luego obtendrá 5.
Y notarás que cuando el tamaño de la pila alcanza un nivel alto, a veces obtienes 50. Esto se debe a que, entonces, se debe tener en cuenta la cantidad de espacio disponible en el montón. Cuando el tamaño de la pila es demasiado grande, tal vez se agote el espacio de montón antes de la pila. Y (tal vez) el tamaño real de los marcos de pila de System.out.println
es aproximadamente 51 veces main
, por lo tanto, retrocede 51 veces e imprime 50.
El comportamiento depende del tamaño de la pila (que se puede establecer manualmente usando Xss
. El tamaño de la pila es específico de la arquitectura. Desde el código fuente JDK 7:
// El tamaño de pila predeterminado en Windows está determinado por el ejecutable (java.exe
// tiene un valor predeterminado de 320K / 1MB [32bit / 64bit]). Dependiendo de la versión de Windows, cambiando
// ThreadStackSize a distinto de cero puede tener un impacto significativo en el uso de la memoria.
// Ver comentarios en os_windows.cpp.
Entonces, cuando se lanza Error
, el error queda atrapado en catch block. Aquí println()
es otra llamada de pila que arroja una excepción de nuevo. Esto se repite.
¿Cuántas veces se repite? - Bueno, depende de cuándo JVM cree que ya no es . Y eso depende del tamaño de la pila de cada llamada de función (difícil de encontrar) y el Xss
. Como se mencionó anteriormente, el tamaño total y el tamaño predeterminados 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í el comportamiento diferente.
Llamar a la llamada java
con -Xss 4M
me da 41
. De ahí la correlación.
Esta es la víctima de una mala llamada recursiva. A medida que se pregunta por qué el valor de cnt varía, 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 here .
Puede ejecutar utilizando diferentes tamaños de pila y verá diferentes valores de cnt antes de que la pila se desborde-
java -Xss1024k RandomNumberGenerator
No ve el valor de cnt que se imprime varias veces aunque el valor sea mayor que 1 a veces porque su extracto de impresión también está generando un error que puede depurar para estar seguro a través de Eclipse u otros IDEs.
Puede cambiar el código a lo siguiente para depurar por ejecución de sentencia si lo prefiere
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) {
}
}
}
ACTUALIZAR:
Como esto recibe mucha más atención, tengamos otro ejemplo para aclarar las cosas ...
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);
}
Creamos otro método llamado overflow para hacer una recursión incorrecta y eliminamos la instrucció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 ejecuta 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 fantasía.
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 fue 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 como sigue
public static void main( String[] args ){
System.out.println(cnt++);
main(args);
}
Entonces verá todos los valores de cnt
y las excepciones lanzadas (en jdk 1.7).
Usé netbeans para ver la salida, ya que el cmd no mostrará todos los resultados y excepciones lanzados.