Дефинирани и демонстрирани клаузи за опита накрая

Добре дошли в друга част от Under The Hood . Тази колона дава на разработчиците на Java поглед върху мистериозните механизми, щракащи и въртящи се под техните работещи Java програми. Статията от този месец продължава обсъждането на набора от инструкции за байт кода на виртуалната машина Java (JVM). Неговият фокус е върху начина, по който JVM обработва finallyклаузи и байт кодове, които са от значение за тези клаузи.

И накрая: Нещо, за което да развеселите

Тъй като Java виртуалната машина изпълнява байт кодовете, които представляват програма на Java, тя може да излезе от блок код - изявленията между две съвпадащи фигурни скоби - по един от няколко начина. От една страна, JVM просто може да изпълни след затварящата се къдрава скоба на блока код. Или може да срещне оператор за прекъсване, продължаване или връщане, което го кара да изскочи от блока с код от някъде в средата на блока. И накрая, може да бъде хвърлено изключение, което кара JVM или да премине към съответстваща клауза клауза, или, ако няма съответстваща клауза клауза, да прекрати нишката. С тези потенциални изходни точки, съществуващи в рамките на един блок код, е желателно да има лесен начин да се изрази, че нещо се е случило, без значение как е излязъл блок от код. В Java такова желание се изразява сtry-finally клауза.

За да използвате try-finallyклауза:

  • да приложи в tryблок кода, който има множество изходни точки, и

  • поставете в finallyблок кода, който трябва да се случи, независимо от начина на tryизлизане от блока.

Например:

try {// Блок код с множество изходни точки} накрая {// Блок код, който винаги се изпълнява при излизане от блока try, // без значение как е изведен блока try} 

Ако имате някакви catchклаузи, свързани с tryблока, трябва да поставите finallyклаузата след всички catchклаузи, както в:

опитайте {// Блок с код с множество изходни точки} catch (Cold e) {System.out.println ("Уловен!"); } catch (APopFly e) {System.out.println ("Хвана поп муха!"); } catch (SomeonesEye e) {System.out.println („Хвана окото!“); } накрая {// Блок от код, който винаги се изпълнява при излизане от блока за опит, // без значение как се излиза от блока за опит. System.out.println („Това за нещо ли е за развеселение?“); }

Ако по време на изпълнение на кода в рамките на tryблок се изведе изключение, което се обработва от catchклауза, свързана с tryблока, finallyклаузата ще бъде изпълнена след catchклаузата. Например, ако по Coldвреме на изпълнението на изразите (не е показано) в tryблока по-горе се изведе изключение , следният текст ще бъде записан в стандартния изход:

Хванат на студ! Това нещо ли е за развеселение?

Опитай клаузи в байт кодове

В байт кодовете finallyклаузите действат като миниатюрни подпрограми в даден метод. Във всяка изходна точка вътре в tryблок и свързаните с него catchклаузи finallyсе извиква миниатюрната подпрограма, която съответства на клаузата. След като finallyклаузата завърши - стига да завърши, като изпълни миналото последно изявление в finallyклаузата, а не като хвърли изключение или изпълни връщане, продължаване или прекъсване - самата миниатюрна подпрограма се връща. Изпълнението продължава точно след точката, в която миниатюрната подпрограма е била извикана на първо място, така че tryблокът може да бъде изведен по подходящ начин.

Опкодът, който кара JVM да премине към миниатюрна подпрограма, е инструкцията jsr . Инструкцията jsr отнема двубайтов операнд, отклонението от местоположението на jsr инструкцията, където започва миниатюрната подпрограма. Втори вариант на jsr инструкцията е jsr_w , който изпълнява същата функция като jsr, но приема широк (четирибайтов) операнд. Когато JVM срещне jsr или jsr_w инструкция, тя избутва връщащ адрес в стека, след което продължава изпълнението в началото на миниатюрната подпрограма. Адресът за връщане е изместването на байт кода непосредствено след jsr илиjsr_w инструкция и нейните операнди.

След като миниатюрна подпрограма завърши, тя извиква инструкцията ret , която се връща от подпрограмата. Инструкцията ret взема един операнд, индекс в локалните променливи, където се съхранява адреса за връщане. Опкодовете, които се занимават с finallyклаузи, са обобщени в следната таблица:

Накрая клаузи
Opcode Операнд (и) Описание
jsr branchbyte1, branchbyte2 избутва адреса за връщане, клони към компенсиране
jsr_w branchbyte1, branchbyte2, branchbyte3, branchbyte4 избутва адреса за връщане, разклонява се на широко разстояние
ret индекс връща се към адреса, съхранен в локалния променлив индекс

Не бъркайте миниатюрна подпрограма с метод Java. Java методите използват различен набор от инструкции. Инструкции като invokevirtual или invokenonvirtual карат Java метод да бъде извикан, а инструкции като return , areturn или ireturn карат Java метод да се върне. Инструкцията jsr не предизвиква извикване на метод Java. Вместо това причинява преход към различен opcode в рамките на същия метод. По същия начин инструкцията ret не се връща от метод; по-скоро се връща обратно към opcode в същия метод, който веднага следва извикващата jsr инструкция и нейните операнди. Байт кодовете, които изпълняват afinallyклауза се наричат ​​миниатюрна подпрограма, тъй като действат като малка подпрограма в потока от байтови кодове на един метод.

Може да си помислите, че инструкцията за извличане трябва да извади адреса за връщане от стека, защото там е била изтласкана от инструкцията jsr . Но това не е така. Вместо това, в началото на всяка подпрограма, връщащият адрес се изскача от горната част на стека и се съхранява в локална променлива - същата локална променлива, от която инструкцията за ret получава по-късно. Този асиметричен начин на работа с обратен адрес е необходимо, защото накрая клаузи (и следователно, миниатюрни подпрограми) самите могат да хвърлят изключения или включват return, breakили continueизявления. Поради тази възможност, допълнителният адрес за връщане, който беше изтласкан в стека от jsrинструкция трябва да бъдат отстранени от комина веднага, така че няма да продължи да бъде там, ако finallyклауза изходите с break, continue, return, или хвърлен изключение. Следователно адресът за връщане се съхранява в локална променлива в началото на finallyминиатюрната подпрограма на която и да е клауза.

Като илюстрация разгледайте следния код, който включва finallyклауза, която излиза с оператор за прекъсване. Резултатът от този код е, че независимо от параметъра bVal, предаден на метода surpriseTheProgrammer(), методът връща false:

статично булево изненадаTheProgrammer (булево bVal) {while (bVal) {try {return true; } накрая {почивка; }} върнете false; }

Примерът по-горе показва защо адресът за връщане трябва да се съхранява в локална променлива в началото на finallyклаузата. Тъй като finallyклаузата излиза с прекъсване, тя никога не изпълнява инструкцията ret . В резултат на това JVM никога не се връща, за да завърши " return true" изявлението. Вместо това, той просто продължава напред breakи пада надолу покрай затварящата къдрава скоба на whileизявлението. Следващото твърдение е „ return false,“, което е точно това, което JVM прави.

Поведението, показано чрез finallyклауза, която излиза с a, breakсе показва и от finallyклаузи, които излизат с returnили continue, или чрез хвърляне на изключение. Ако finallyклауза излезе по някоя от тези причини, инструкцията за повторение в края на finallyклаузата никога не се изпълнява. Тъй като инструкцията за ret не е гарантирана за изпълнение, на нея не може да се разчита за премахване на адреса за връщане от стека. Следователно адресът за връщане се съхранява в локална променлива в началото на finallyминиатюрната подпрограма на клаузата.

За пълен пример разгледайте следния метод, който съдържа tryблок с две изходни точки. В този пример и двете изходни точки са returnизявления:

static int giveMeThatOldFashionedBoolean (boolean bVal) {try {if (bVal) {return 1; } връщане 0; } накрая {System.out.println ("Старомоден."); }}

Горният метод се компилира в следните байт кодове:

// 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:

клас Clown {static int hopAround () {int i = 0; while (true) {опитайте {опитайте {i = 1; } накрая {// първата клауза накрая i = 2; } i = 3; return i; // това никога не завършва, поради continue} окончателно {// втората клауза окончателно if (i == 3) {continue; // това продължение заменя оператора за връщане}}}}}

Байт кодовете, генерирани от javacза hopAround()метода, са показани по-долу: