Добре дошли в друга част от 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()
метода, са показани по-долу: