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 finallyund 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-finallyKlausel:

  • Schließen Sie tryden Code mit mehreren Ausstiegspunkten in einen Block ein

  • Fügen Sie in einen finallyBlock den Code ein, der passieren muss, unabhängig davon, wie der tryBlock 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 catchdem tryBlock Klauseln zugeordnet sind, müssen Sie die finallyKlausel nach allen catchKlauseln 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 tryBlocks eine Ausnahme ausgelöst wird, die von einer catchdem tryBlock zugeordneten finallyKlausel behandelt wird , wird die Klausel nach der catchKlausel ausgeführt. Wenn beispielsweise Coldwährend der Ausführung der Anweisungen (nicht gezeigt) im tryobigen 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 finallyfungieren Klauseln als Miniatur-Unterprogramme innerhalb einer Methode. An jedem Austrittspunkt innerhalb eines tryBlocks und den zugehörigen catchKlauseln wird die Miniatur-Subroutine finallyaufgerufen , die der Klausel entspricht . Nachdem die finallyKlausel abgeschlossen ist - solange sie durch Ausführen nach der letzten Anweisung in der finallyKlausel 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 tryursprü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 finallyKlauseln befassen , sind in der folgenden Tabelle zusammengefasst:

Endlich Klauseln
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 implementierenfinallyKlauseln 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, breakoder continueAussagen. 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 finallyKlausel endet mit einer break, continue, return, oder geworfen Ausnahme. Daher wird die Rücksprungadresse zu Beginn der finallyMiniatur-Subroutine einer Klausel in einer lokalen Variablen gespeichert .

Betrachten Sie zur Veranschaulichung den folgenden Code, der eine finallyKlausel 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 javacfür die hopAround()Methode generierten Bytecodes sind nachstehend aufgeführt: