![]() |
Sponsored Link •
|
Advertisement
|
Try-finally clauses in bytecodes
In bytecodes, finally
clauses act as miniature subroutines
within a method. At each exit point inside a try
block and
its associated catch
clauses, the miniature subroutine
that corresponds to the finally
clause is called. After
the finally
clause completes -- as long as it completes by
executing past the last statement in the finally
clause,
not by throwing an exception or executing a return, continue, or break
-- the miniature subroutine itself returns. Execution continues just
past the point where the miniature subroutine was called in the first
place, so the try
block can be exited in the appropriate
manner.
The opcode that causes the JVM to jump to a miniature subroutine is the jsr instruction. The jsr instruction takes a two-byte operand, the offset from the location of the jsr instruction where the miniature subroutine begins. A second variant of the jsr instruction is jsr_w, which performs the same function as jsr but takes a wide (four-byte) operand. When the JVM encounters a jsr or jsr_w instruction, it pushes a return address onto the stack, then continues execution at the start of the miniature subroutine. The return address is the offset of the bytecode immediately following the jsr or jsr_w instruction and its operands.
After a miniature subroutine completes, it invokes the ret
instruction, which returns from the subroutine. The ret
instruction takes one operand, an index into the local variables where
the return address is stored. The opcodes that deal with
finally
clauses are summarized in the following table:
Opcode | Operand(s) | Description |
---|---|---|
jsr |
branchbyte1, branchbyte2 | pushes the return address, branches to offset |
jsr_w |
branchbyte1, branchbyte2, branchbyte3, branchbyte4 | pushes the return address, branches to wide offset |
ret |
index | returns to the address stored in local variable index |
Don't confuse a miniature subroutine with a Java method. Java methods
use a different set of instructions. Instructions such as
invokevirtual or invokenonvirtual cause a Java method
to be invoked, and instructions such as return,
areturn, or ireturn cause a Java method to return.
The jsr instruction does not cause a Java method to be
invoked. Instead, it causes a jump to a
different opcode within the same method. Likewise, the ret instruction
doesn't return from a method; rather, it returns back to the
opcode in the same method that immediately follows the calling
jsr instruction and its operands. The bytecodes that implement
a finally
clause are called a miniature subroutine because
they act like a small subroutine within the bytecode stream of a single
method.
You might think that the ret instruction should pop the return
address off the stack, because that is where it was pushed by the
jsr instruction. But it doesn't. Instead, at the start of each
subroutine, the return address is popped off the top of the stack and
stored in a local variable -- the same local variable from which the
ret instruction later gets it. This asymmetrical manner of
working with the return address is necessary because finally clauses
(and therefore, miniature subroutines) themselves can throw exceptions
or include return
, break
, or
continue
statements. Because of this possibility, the
extra return address that was pushed onto the stack by the jsr
instruction must be removed from the stack right away, so it won't
still be there if the finally
clause exits with a
break
, continue
, return
, or
thrown exception. Therefore, the return address is stored into a local
variable at the start of any finally
clause's miniature subroutine.
As an illustration, consider the following code, which includes a
finally
clause that exits with a break statement. The
result of this code is that, irrespective of the parameter bVal passed
to method surpriseTheProgrammer()
, the method returns
false
:
static boolean surpriseTheProgrammer(boolean bVal) {
while (bVal) {
try {
return true;
}
finally {
break;
}
}
return false;
}
The example above shows why the return address must be stored into a
local variable at the beginning of the finally
clause.
Because the finally
clause exits with a break, it never
executes the ret instruction. As a result, the JVM
never goes back to finish up the "return true
"
statement. Instead, it just goes ahead with the break
and
drops down past the closing curly brace of the while
statement. The next statement is "return false
," which is
precisely what the JVM does.
The behavior shown by a finally
clause that exits with a
break
is also shown by finally
clauses that
exit with a return
or continue
, or by throwing
an exception. If a finally
clause exits for any of
these reasons, the ret instruction at the end of the
finally
clause is never executed. Because the ret
instruction is not guaranteed to be executed, it can't be relied on
to remove the return address from the stack. Therefore, the return
address is stored into a local variable at the beginning of the
finally
clause's miniature subroutine.
For a complete example, consider the following method, which contains a
try
block with two exit points. In this example, both exit
points are return
statements:
static int giveMeThatOldFashionedBoolean(boolean bVal) {
try {
if (bVal) {
return 1;
}
return 0;
}
finally {
System.out.println("Got old fashioned.");
}
}
The above method compiles to the following bytecodes:
// The bytecode sequence for the try block:
0 iload_0 // Push local variable 0 (arg passed as divisor)
1 ifeq 11 // Push local variable 1 (arg passed as dividend)
4 iconst_1 // Push int 1
5 istore_3 // Pop an int (the 1), store into local variable 3
6 jsr 24 // Jump to the mini-subroutine for the finally clause
9 iload_3 // Push local variable 3 (the 1)
10 ireturn // Return int on top of the 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 // Jump to the mini-subroutine for the finally clause
16 iload_3 // Push local variable 3 (the 0)
17 ireturn // Return int on top of the stack (the 0)
// The bytecode sequence for a catch clause that catches any kind of exception
// thrown from within the try block.
18 astore_1 // Pop the reference to the thrown exception, store
// into local variable 1
19 jsr 24 // Jump to the mini-subroutine for the finally clause
22 aload_1 // Push the reference (to the thrown exception) from
// local variable 1
23 athrow // Rethrow the same exception
// The miniature subroutine that implements the finally block.
24 astore_2 // Pop the return address, store it in local variable 2
25 getstatic #8 // Get a reference to java.lang.System.out
28 ldc #1 // Push <String "Got old fashioned."> from the constant pool
30 invokevirtual #7 // Invoke System.out.println()
33 ret 2 // Return to return address stored in local variable 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.
Sponsored Links
|