Try-finally-Klauseln definiert und demonstriert
Willkommen zu einer weiteren Ausgabe von Under The Hood . Diese Spalte gibt Java-Entwicklern einen Einblick in die mysteriösen Mechanismen, die unter ihren laufenden Java-Programmen klicken und surren. Der Artikel dieses Monats setzt die Diskussion des Bytecode-Befehlssatzes der Java Virtual Machine (JVM) fort. Der Schwerpunkt liegt auf der Art finally
und Weise, wie die JVM mit Klauseln und den für diese Klauseln relevanten Bytecodes umgeht.
Endlich: Etwas zum Jubeln
Während die virtuelle Java-Maschine die Bytecodes ausführt, die ein Java-Programm darstellen, kann sie einen Codeblock - die Anweisungen zwischen zwei übereinstimmenden geschweiften Klammern - auf eine von mehreren Arten beenden. Zum einen könnte die JVM einfach über die schließende geschweifte Klammer des Codeblocks hinaus ausgeführt werden. Oder es kann zu einer Unterbrechungs-, Fortsetzungs- oder Rückgabeanweisung kommen, die dazu führt, dass es von irgendwo in der Mitte des Blocks aus dem Codeblock springt. Schließlich könnte eine Ausnahme ausgelöst werden, die dazu führt, dass die JVM entweder zu einer übereinstimmenden catch-Klausel springt oder den Thread beendet, wenn keine übereinstimmende catch-Klausel vorhanden ist. Da diese potenziellen Austrittspunkte in einem einzelnen Codeblock vorhanden sind, ist es wünschenswert, auf einfache Weise auszudrücken, dass etwas passiert ist, unabhängig davon, wie ein Codeblock verlassen wird. In Java wird ein solcher Wunsch mit a ausgedrückttry-finally
Klausel.
So verwenden Sie eine try-finally
Klausel:
Schließen Sie
try
den Code mit mehreren Ausstiegspunkten in einen Block einFügen Sie in einen
finally
Block den Code ein, der passieren muss, unabhängig davon, wie dertry
Block verlassen wird.
Zum Beispiel:
try {// Codeblock mit mehreren Austrittspunkten} endlich {// Codeblock, der immer ausgeführt wird, wenn der try-Block beendet wird, // unabhängig davon, wie der try-Block beendet wird}
Wenn catch
dem try
Block Klauseln zugeordnet sind, müssen Sie die finally
Klausel nach allen catch
Klauseln einfügen, wie in:
try {// Codeblock mit mehreren Austrittspunkten} catch (Cold e) {System.out.println ("Caught kalt!"); } catch (APopFly e) {System.out.println ("Eine Popfliege erwischt!"); } catch (SomeonesEye e) {System.out.println ("Jemandes Auge gefangen!"); } finally {// Codeblock, der immer ausgeführt wird, wenn der try-Block beendet wird, // unabhängig davon, wie der try-Block beendet wird. System.out.println ("Ist das etwas, worüber man sich freuen kann?"); }}
Wenn während der Ausführung des Codes innerhalb eines try
Blocks eine Ausnahme ausgelöst wird, die von einer catch
dem try
Block zugeordneten finally
Klausel behandelt wird , wird die Klausel nach der catch
Klausel ausgeführt. Wenn beispielsweise Cold
während der Ausführung der Anweisungen (nicht gezeigt) im try
obigen Block eine Ausnahme ausgelöst wird , wird der folgende Text in die Standardausgabe geschrieben:
Erkältet! Ist das etwas zum Jubeln?
Try-finally-Klauseln in Bytecodes
In Bytecodes finally
fungieren Klauseln als Miniatur-Unterprogramme innerhalb einer Methode. An jedem Austrittspunkt innerhalb eines try
Blocks und den zugehörigen catch
Klauseln wird die Miniatur-Subroutine finally
aufgerufen , die der Klausel entspricht . Nachdem die finally
Klausel abgeschlossen ist - solange sie durch Ausführen nach der letzten Anweisung in der finally
Klausel ausgeführt wird, nicht durch Auslösen einer Ausnahme oder Ausführen einer Rückgabe, Fortfahren oder Unterbrechen -, wird die Miniatur-Subroutine selbst zurückgegeben. Die Ausführung wird unmittelbar nach dem Punkt fortgesetzt, an dem das Miniatur-Unterprogramm try
ursprünglich aufgerufen wurde, sodass der Block auf die entsprechende Weise verlassen werden kann.
Der Opcode, der die JVM veranlasst, zu einer Miniatur-Subroutine zu springen, ist der Befehl jsr . Der jsr- Befehl nimmt einen Zwei-Byte-Operanden an, der vom Ort des jsr- Befehls versetzt ist, an dem die Miniatur-Subroutine beginnt. Eine zweite Variante des Befehls jsr ist jsr_w , der dieselbe Funktion wie jsr ausführt, jedoch einen breiten Operanden (vier Byte) verwendet. Wenn die JVM auf einen Befehl jsr oder jsr_w stößt, schiebt sie eine Rücksprungadresse auf den Stapel und setzt die Ausführung am Anfang der Miniatur-Subroutine fort. Die Absenderadresse ist der Offset des Bytecodes unmittelbar nach jsr oderBefehl jsr_w und seine Operanden.
Nachdem eine Miniatur-Subroutine abgeschlossen ist, ruft sie den Befehl ret auf , der von der Subroutine zurückkehrt. Der Befehl ret nimmt einen Operanden, einen Index in die lokalen Variablen, in denen die Rücksprungadresse gespeichert ist. Die Opcodes, die sich mit finally
Klauseln befassen , sind in der folgenden Tabelle zusammengefasst:
Opcode | Operand (en) | Beschreibung |
---|---|---|
jsr |
branchbyte1, branchbyte2 | schiebt die Absenderadresse, verzweigt zum Offset |
jsr_w |
branchbyte1, branchbyte2, branchbyte3, branchbyte4 | schiebt die Absenderadresse, verzweigt zu weitem Versatz |
ret |
Index | kehrt zu der im lokalen Variablenindex gespeicherten Adresse zurück |
Verwechseln Sie eine Miniatur-Subroutine nicht mit einer Java-Methode. Java-Methoden verwenden einen anderen Befehlssatz. Anweisungen wie invokevirtual oder invokenonvirtual bewirken, dass eine Java-Methode aufgerufen wird, und Anweisungen wie return , areturn oder ireturn bewirken, dass eine Java-Methode zurückgegeben wird. Die Anweisung jsr bewirkt nicht, dass eine Java-Methode aufgerufen wird. Stattdessen wird innerhalb derselben Methode zu einem anderen Opcode gesprungen. Ebenso kehrt der Befehl ret nicht von einer Methode zurück. Vielmehr kehrt es mit derselben Methode zum Opcode zurück, die unmittelbar auf den aufrufenden Befehl jsr und seine Operanden folgt . Die Bytecodes, die a implementierenfinally
Klauseln werden als Miniatur-Subroutine bezeichnet, da sie sich wie eine kleine Subroutine innerhalb des Bytecode-Streams einer einzelnen Methode verhalten.
Sie könnten denken, dass der Befehl ret die Rücksprungadresse vom Stapel entfernen sollte , da sie dort vom Befehl jsr verschoben wurde . Aber das tut es nicht. Stattdessen wird zu Beginn jeder Unterroutine die Rücksprungadresse oben im Stapel abgelegt und in einer lokalen Variablen gespeichert - derselben lokalen Variablen, von der der Befehl ret sie später erhält. Diese asymmetrische Art und Weise mit der Absenderadresse des Arbeits ist notwendig , weil schließlich Klauseln (und daher Miniatur - Subroutinen) selbst Ausnahmen auslösen können oder sind return
, break
oder continue
Aussagen. Aufgrund dieser Möglichkeit wurde die zusätzliche Rücksprungadresse von jsr auf den Stapel geschobenAusbildung muss sofort vom Stapel entfernt werden, so dass es nicht immer noch da sein wird , wenn die finally
Klausel endet mit einer break
, continue
, return
, oder geworfen Ausnahme. Daher wird die Rücksprungadresse zu Beginn der finally
Miniatur-Subroutine einer Klausel in einer lokalen Variablen gespeichert .
Betrachten Sie zur Veranschaulichung den folgenden Code, der eine finally
Klausel enthält, die mit einer break-Anweisung beendet wird. Das Ergebnis dieses Codes ist, dass surpriseTheProgrammer()
die Methode unabhängig vom an die Methode übergebenen Parameter bVal Folgendes zurückgibt false
:
statische boolesche ÜberraschungTheProgrammer (boolesche bVal) {while (bVal) {try {return true; } endlich {Pause; } } falsch zurückgeben; }}
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 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.
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:
Klasse Clown {statisch int hopAround () {int i = 0; while (true) {try {try {i = 1; } finally {// die erste finally-Klausel i = 2; } i = 3; return i; // dies wird wegen der continue} finally {// der zweiten finally-Klausel nie abgeschlossen, wenn (i == 3) {continue; // diese Fortsetzung überschreibt die return-Anweisung}}}}}
Die von javac
für die hopAround()
Methode generierten Bytecodes sind nachstehend aufgeführt: