Bienvenido a otra entrega de Under The Hood . Esta columna ofrece a los desarrolladores de Java una idea de los misteriosos mecanismos que hacen clic y zumban debajo de sus programas Java en ejecución. El artículo de este mes continúa la discusión del conjunto de instrucciones de código de bytes de la máquina virtual Java (JVM). Su enfoque es la forma en que la JVM maneja las finally
cláusulas y los códigos de bytes que son relevantes para estas cláusulas.
Finalmente: algo para alegrar
A medida que la máquina virtual Java ejecuta los códigos de bytes que representan un programa Java, puede salir de un bloque de código (las declaraciones entre dos llaves coincidentes) de una de varias formas. Por un lado, la JVM simplemente podría ejecutarse más allá de la llave de cierre del bloque de código. O bien, podría encontrar una declaración de interrupción, continuación o devolución que haga que salte del bloque de código desde algún lugar en el medio del bloque. Finalmente, se podría lanzar una excepción que haga que la JVM salte a una cláusula catch coincidente o, si no hay una cláusula catch coincidente, que termine el hilo. Dado que estos puntos de salida potenciales existen dentro de un solo bloque de código, es deseable tener una manera fácil de expresar que algo sucedió sin importar cómo se salga de un bloque de código. En Java, tal deseo se expresa con untry-finally
cláusula.
Para usar una try-finally
cláusula:
encierre en un
try
bloque el código que tiene múltiples puntos de salida, yponga en un
finally
bloque el código que debe suceder sin importar cómotry
se salga del bloque.
Por ejemplo:
try {// Bloque de código con múltiples puntos de salida} finalmente {// Bloque de código que siempre se ejecuta cuando se sale del bloque try, // no importa cómo se sale del bloque try}
Si tiene alguna catch
cláusula asociada con el try
bloque, debe poner la finally
cláusula después de todas las catch
cláusulas, como en:
try {// Bloque de código con múltiples puntos de salida} catch (Cold e) {System.out.println ("¡Atrapado en frío!"); } catch (APopFly e) {System.out.println ("¡Atrapé una mosca pop!"); } catch (SomeonesEye e) {System.out.println ("¡Le llamó la atención a alguien!"); } finalmente {// Bloque de código que siempre se ejecuta cuando se sale del bloque try, // sin importar cómo se sale del bloque try. System.out.println ("¿Es eso algo por lo que alegrarse?"); }
Si durante la ejecución del código dentro de un try
bloque, se lanza una excepción que es manejada por una catch
cláusula asociada con el try
bloque, la finally
cláusula se ejecutará después de la catch
cláusula. Por ejemplo, si Cold
se lanza una excepción durante la ejecución de las declaraciones (no mostradas) en el try
bloque anterior, el siguiente texto se escribiría en la salida estándar:
¡Me resfrie! ¿Es eso algo por lo que alegrarse?
Cláusulas de prueba finalmente en códigos de bytes
En los códigos de bytes, las finally
cláusulas actúan como subrutinas en miniatura dentro de un método. En cada punto de salida dentro de un try
bloque y sus catch
cláusulas asociadas , finally
se llama a la subrutina en miniatura que corresponde a la cláusula. Una vez que la finally
cláusula se completa, siempre que se complete ejecutando más allá de la última declaración de la finally
cláusula, no lanzando una excepción o ejecutando un retorno, continuar o romper, la subrutina en miniatura en sí regresa. La ejecución continúa justo después del punto donde se llamó a la subrutina en miniatura en primer lugar, por lo try
que se puede salir del bloque de la manera adecuada.
El código de operación que hace que la JVM salte a una subrutina en miniatura es la instrucción jsr . La instrucción jsr toma un operando de dos bytes, el desplazamiento desde la ubicación de la instrucción jsr donde comienza la subrutina en miniatura. Una segunda variante de la instrucción jsr es jsr_w , que realiza la misma función que jsr pero toma un operando ancho (cuatro bytes). Cuando la JVM encuentra una instrucción jsr o jsr_w , inserta una dirección de retorno en la pila y luego continúa la ejecución al comienzo de la subrutina en miniatura. La dirección de retorno es el desplazamiento del código de bytes que sigue inmediatamente al jsr oinstrucción jsr_w y sus operandos.
Después de que se completa una subrutina en miniatura, invoca la instrucción ret , que regresa de la subrutina. La instrucción ret toma un operando, un índice en las variables locales donde se almacena la dirección de retorno. Los códigos de operación que tratan con finally
cláusulas se resumen en la siguiente tabla:
Código de operación | Operando (s) | Descripción |
---|---|---|
jsr |
branchbyte1, branchbyte2 | empuja la dirección de retorno, se ramifica para compensar |
jsr_w |
branchbyte1, branchbyte2, branchbyte3, branchbyte4 | empuja la dirección de retorno, se ramifica a un desplazamiento amplio |
ret |
índice | vuelve a la dirección almacenada en el índice de la variable local |
No confunda una subrutina en miniatura con un método Java. Los métodos de Java utilizan un conjunto de instrucciones diferente. Las instrucciones como invokevirtual o invokenonvirtual hacen que se invoque un método Java, y las instrucciones como return , areturn o ireturn hacen que regrese un método Java. La instrucción jsr no hace que se invoque un método Java. En cambio, provoca un salto a un código de operación diferente dentro del mismo método. Asimismo, la instrucción ret no regresa de un método; más bien, regresa al código de operación en el mismo método que sigue inmediatamente a la instrucción jsr que llama y sus operandos. Los códigos de bytes que implementan unfinally
Las cláusulas se denominan subrutina en miniatura porque actúan como una pequeña subrutina dentro del flujo de código de bytes de un solo método.
Podría pensar que la instrucción ret debería sacar la dirección de retorno de la pila, porque ahí es donde fue empujada por la instrucción jsr . Pero no es así. En cambio, al comienzo de cada subrutina, la dirección de retorno se extrae de la parte superior de la pila y se almacena en una variable local, la misma variable local de la que luego la obtiene la instrucción ret . Es necesaria esta manera asimétrica de trabajo con la dirección de retorno, porque finalmente las cláusulas (y por lo tanto, subrutinas en miniatura) sí pueden lanzar excepciones o incluir return
, break
o continue
declaraciones. Debido a esta posibilidad, la dirección de retorno adicional que fue empujada a la pila por jsrla instrucción debe ser retirado de la pila de inmediato, por lo que no todavía estará allí si las finally
salidas con una cláusula break
, continue
, return
, o excepción lanzada. Por lo tanto, la dirección de retorno se almacena en una variable local al comienzo de finally
la subrutina en miniatura de cualquier cláusula.
Como ilustración, considere el siguiente código, que incluye una finally
cláusula que sale con una declaración de interrupción. El resultado de este código es que, independientemente del parámetro bVal pasado al método surpriseTheProgrammer()
, el método devuelve false
:
static boolean surpriseTheProgrammer (boolean bVal) {while (bVal) {try {return true; } finalmente {romper; } } falso retorno; }
El ejemplo anterior muestra por qué la dirección de retorno debe almacenarse en una variable local al comienzo de la finally
cláusula. Debido a que la finally
cláusula sale con una interrupción, nunca ejecuta la instrucción ret . Como resultado, la JVM nunca regresa para terminar la return true
declaración " ". En su lugar, sigue adelante con break
y pasa por debajo de la llave de cierre de la while
declaración. La siguiente declaración es " return false
," que es precisamente lo que hace la JVM.
El comportamiento que muestra una finally
cláusula que sale con a break
también se muestra mediante finally
cláusulas que salen con a return
o continue
, o lanzando una excepción. Si una finally
cláusula sale por cualquiera de estos motivos, la instrucción ret al final de la finally
cláusula nunca se ejecuta. Debido a que no se garantiza que la instrucción ret se ejecute, no se puede confiar en ella para eliminar la dirección de retorno de la pila. Por lo tanto, la dirección de retorno se almacena en una variable local al comienzo de la finally
subrutina en miniatura de la cláusula.
Para obtener un ejemplo completo, considere el siguiente método, que contiene un try
bloque con dos puntos de salida. En este ejemplo, ambos puntos de salida son return
declaraciones:
static int giveMeThatOldFashionedBoolean (bVal booleano) {try {if (bVal) {return 1; } return 0; } finalmente {System.out.println ("Se pasó de moda"); }}
El método anterior se compila con los siguientes códigos de bytes:
// La secuencia de código de bytes para el bloque try: 0 iload_0 // Insertar la variable local 0 (arg se pasa como divisor) 1 ifeq 11 // Insertar la variable local 1 (arg se pasa como dividendo) 4 iconst_1 // Insertar int 1 5 istore_3 // Haga estallar un int (el 1), almacénelo en la variable local 3 6 jsr 24 // Salte a la mini-subrutina para la cláusula finalmente 9 iload_3 // Inserte la variable local 3 (el 1) 10 ireturn // Regrese int encima stack (the 1) 11 iconst_0 // Push int 0 12 istore_3 // Pop an int (the 0), store into local variable 3 13 jsr 24 // Salta a la mini-subrutina para la cláusula finalmente 16 iload_3 // Push local variable 3 (el 0) 17 ireturn // Devuelve int encima de la pila (el 0) // La secuencia de código de bytes para una cláusula catch que detecta cualquier tipo de excepción // lanzada desde dentro del bloque try. 18 astore_1 // Abre la referencia a la excepción lanzada,almacenar // en la variable local 1 19 jsr 24 // Saltar a la mini-subrutina para la cláusula finalmente 22 aload_1 // Insertar la referencia (a la excepción lanzada) desde // la variable local 1 23 en la fila // Relanzar la misma excepción / / La subrutina en miniatura que implementa el bloque finalmente. 24 astore_2 // Pop la dirección de retorno, guárdela en la variable local 2 25 getstatic # 8 // Obtenga una referencia a java.lang.System.out 28 ldc # 1 // Empuje desde el grupo constante 30 invokevirtual # 7 // Invoque System.out.println () 33 ret 2 // Regresar a la dirección de retorno almacenada en la variable local 2almacenarlo en la variable local 2 25 getstatic # 8 // Obtener una referencia a java.lang.System.out 28 ldc # 1 // Insertar desde el grupo constante 30 invokevirtual # 7 // Invocar System.out.println () 33 ret 2 // Volver a la dirección de retorno almacenada en la variable local 2almacenarlo en la variable local 2 25 getstatic # 8 // Obtener una referencia a java.lang.System.out 28 ldc # 1 // Insertar desde el grupo constante 30 invokevirtual # 7 // Invocar System.out.println () 33 ret 2 // Volver a la dirección de retorno almacenada en la variable local 2
The bytecodes for the try
block include two jsr instructions. Another jsr instruction is contained in the catch
clause. The catch
clause is added by the compiler because if an exception is thrown during the execution of the try
block, the finally block must still be executed. Therefore, the catch
clause merely invokes the miniature subroutine that represents the finally
clause, then throws the same exception again. The exception table for the giveMeThatOldFashionedBoolean()
method, shown below, indicates that any exception thrown between and including addresses 0 and 17 (all the bytecodes that implement the try
block) are handled by the catch
clause that starts at address 18.
Exception table: from to target type 0 18 18 any
The bytecodes of the finally
clause begin by popping the return address off the stack and storing it into local variable two. At the end of the finally
clause, the ret instruction takes its return address from the proper place, local variable two.
HopAround: A Java virtual machine simulation
The applet below demonstrates a Java virtual machine executing a sequence of bytecodes. The bytecode sequence in the simulation was generated by the javac
compiler for the hopAround()
method of the class shown below:
clase Payaso {static int hopAround () {int i = 0; while (verdadero) {prueba {prueba {i = 1; } finalmente {// la primera cláusula final i = 2; } i = 3; volver i; // esto nunca se completa, debido a la continuación} finalmente {// la segunda cláusula finalmente si (i == 3) {continuar; // esta continuación anula la declaración de retorno}}}}}
Los códigos de bytes generados por javac
para el hopAround()
método se muestran a continuación: