programa - ¿Por qué la copia del compilador de Java finalmente se bloquea?
que es jvm (2)
Alineando finalmente los bloques
La pregunta que hizo su pregunta ha sido analizada en parte en: http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/
La publicación mostrará un ejemplo interesante, así como información como (cita):
los bloques finalmente se implementan al alinear el código finalmente en todas las salidas posibles de los bloques try o asociados catch, envolviendo todo en esencialmente un bloque "catch (Throwable)" que vuelve a generar la excepción cuando finaliza, y luego ajustando la tabla de excepciones como que las cláusulas catch se saltan las declaraciones finalmente en línea. Eh (Pequeña advertencia: antes del compilador 1.6, aparentemente, finalmente las declaraciones usaban sub-rutinas en lugar de un código completo en línea. Pero solo nos preocupa el 1.6 en este punto, así que eso es lo que se aplica a).
La instrucción JSR y en línea finalmente
Hay diferentes opiniones acerca de por qué se utiliza la inscripción, aunque todavía no he encontrado una definitiva de un documento oficial o fuente.
Hay las siguientes 3 explicaciones:
Sin ventajas de oferta - más problemas:
Algunos creen que finalmente se usa la alineación porque JSR / RET no ofreció grandes ventajas, como la cita de ¿Qué compiladores de Java usan la instrucción jsr, y para qué?
El mecanismo JSR / RET se usó originalmente para implementar finalmente los bloques. Sin embargo, decidieron que el ahorro en el tamaño del código no valía la complejidad adicional y se fue reduciendo gradualmente.
Problemas con la verificación utilizando tablas de mapa de pila:
Otra explicación posible ha sido propuesta en los comentarios de @ jeffrey-bosboom, a quienes cito a continuación:
javac solía usar jsr (subrutina de salto) para escribir finalmente el código una vez, pero había algunos problemas relacionados con la nueva verificación al usar tablas de mapas de pila. Supongo que volvieron a clonar el código solo porque era lo más fácil de hacer.
Tener que mantener los bits sucios de subrutinas:
Un intercambio interesante en los comentarios de la pregunta ¿Qué compiladores Java utilizan la instrucción jsr y para qué? los puntos que JSR y las subrutinas "añadieron una complejidad adicional al tener que mantener una pila de bits sucios para las variables locales".
Debajo del intercambio:
@ paj28: ¿El JSR habría planteado tales dificultades si solo pudiera llamar "subrutinas" declaradas, cada una de las cuales solo podría ingresarse al comienzo, solo se podría llamar desde otra subrutina, y solo podría salir por medio de la repetición o la finalización abrupta ( volver o tirar)? Duplicar el código en los bloques finalmente parece realmente feo, especialmente porque la limpieza finalmente relacionada puede invocar bloques de prueba anidados. - Supercat 28 de enero de 14 a las 23:18
@supercat, la mayoría de eso ya es cierto. Las subrutinas solo pueden ingresarse desde el principio, solo pueden regresar desde un lugar y solo pueden llamarse desde una única subrutina. La complejidad proviene del hecho de que debe mantener una pila de bits sucios para las variables locales y, al regresar, debe realizar una combinación de tres vías. - Antimonio enero 28 ''14 a las 23:40
Al compilar el siguiente código con un simple bloque try/finally
, el compilador de Java produce la salida a continuación (vista en el Visor de Bytecode de ASM):
Código:
try
{
System.out.println("Attempting to divide by zero...");
System.out.println(1 / 0);
}
finally
{
System.out.println("Finally...");
}
Bytecode:
TRYCATCHBLOCK L0 L1 L1
L0
LINENUMBER 10 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Attempting to divide by zero..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 11 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
ICONST_0
IDIV
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L3
LINENUMBER 12 L3
GOTO L4
L1
LINENUMBER 14 L1
FRAME SAME1 java/lang/Throwable
ASTORE 1
L5
LINENUMBER 15 L5
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L6
LINENUMBER 16 L6
ALOAD 1
ATHROW
L4
LINENUMBER 15 L4
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L7
LINENUMBER 17 L7
RETURN
L8
LOCALVARIABLE args [Ljava/lang/String; L0 L8 0
MAXSTACK = 3
MAXLOCALS = 2
Al agregar un bloque catch
en medio, me di cuenta de que el compilador copió el bloque final 3 veces (sin publicar el código de bytes nuevamente). Esto parece una pérdida de espacio en el archivo de clase. La copia tampoco parece estar limitada a un número máximo de instrucciones (similar a la forma en que funciona la integración), ya que incluso duplicó el bloque final cuando agregué más llamadas a System.out.println
.
Sin embargo, el resultado de un compilador mío personalizado que utiliza un enfoque diferente para compilar el mismo código funciona exactamente igual cuando se ejecuta, pero requiere menos espacio al usar la instrucción GOTO
:
public static main([Ljava/lang/String;)V
// parameter args
TRYCATCHBLOCK L0 L1 L1
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Attempting to divide by zero..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
ICONST_0
IDIV
INVOKEVIRTUAL java/io/PrintStream.println (I)V
GOTO L2
L1
FRAME SAME1 java/lang/Throwable
POP
L2
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
RETURN
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
¿Por qué el compilador de Java (o el compilador de Eclipse) copia el athrow
de athrow
bloque athrow
varias veces, incluso utilizando athrow
para volver a athrow
excepciones, cuando se puede lograr la misma semántica utilizando goto
? ¿Esto es parte del proceso de optimización o mi compilador lo está haciendo mal?
(La salida en ambos casos es ...)
Attempting to divide by zero...
Finally...
Compilando esto:
public static void main(String... args){
try
{
System.out.println("Attempting to divide by zero...");
System.out.println(1 / 0);
}catch(Exception e){
System.out.println("Exception!");
}
finally
{
System.out.println("Finally...");
}
}
Y mirando el resultado de javap -v, el bloque finally se adjunta simplemente al final de cada sección que maneja una excepción (agregando el catch, se agrega un bloque finally en la línea 37, el de 49 es para java.lang sin marcar. Errores):
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=3, locals=3, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Attempting to divide by zero...
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iconst_1
12: iconst_0
13: idiv
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #6 // String Finally...
22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: goto 59
28: astore_1
29: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
32: ldc #8 // String Exception!
34: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
40: ldc #6 // String Finally...
42: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: goto 59
48: astore_2
49: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
52: ldc #6 // String Finally...
54: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
57: aload_2
58: athrow
59: return
Exception table:
from to target type
0 17 28 Class java/lang/Exception
0 17 48 any
28 37 48 any
Parece que la implementación original de finalmente los bloques se parecía a lo que estás proponiendo, pero desde que Java 1.4.2 javac comenzó a integrar bloques finalmente, desde " Una evaluación de los decompiladores actuales de bytecode Java " [2009] de Hamilton & Danicic:
Muchos de los antiguos descompiladores esperan el uso de subrutinas para los bloques try-finally pero javac 1.4.2+ genera un código en línea en su lugar.
Una entrada de blog de 2006 que discute esto:
El código en las líneas 5-12 es idéntico al código en las líneas 19-26, que en realidad se traduce en la línea count ++. El bloque finalmente está claramente copiado.