Java虛擬機(jī)的指令是由一個(gè)字節(jié)長(zhǎng)度的鲸睛、代表著某種特定操作含義的數(shù)字(稱為操作碼箱舞,Opcode)以及跟隨其后的零個(gè)或多個(gè)代表此操作所需參數(shù)(稱為操作數(shù),Operand)而構(gòu)成电湘。由于一條虛擬機(jī)指令的操作碼只用一個(gè)字節(jié)存儲(chǔ),因此Java虛擬機(jī)所能支持的指令最多256條贷痪。
0. 始于Hello World
使用Java代碼編寫(xiě)的Hello World程序如代碼
public class HelloWord {
public static void main(String[] args) {
System.out.println("Hello Word");
}
}
使用javap命令輸出Hello World程序的字節(jié)碼如下
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello Word>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
getstatic指令的操作碼是0xB2,該指令需要一個(gè)操作數(shù)胖缤,該操作數(shù)是常量池中某個(gè)CONSTANT_Fieldref_info常量的索引狗唉。在本例中分俯,該指令表示獲取System的out靜態(tài)字段,該靜態(tài)字段的類(lèi)型為java.io.PrintStream杏节。該指令執(zhí)行完成后奋渔,操作數(shù)棧頂存放的就是System的out靜態(tài)字段的引用
ldc指令的操作碼是0x12歹啼,該指令也需要一個(gè)操作數(shù)藤树,值為常量池中的某個(gè)CONSTANT_String_info常量的索引巡莹。在本例中,其作用是將常量池中的“HelloWord”字符串的引用放入操作數(shù)棧頂腰根。該指令執(zhí)行完后,操作數(shù)棧頂存放的就是字符串“Hello Word”的引用册养,如圖
invokevirtual指令的操作碼是0xB6,該指令也需要一個(gè)操作數(shù),值為常量池中某個(gè)CONSTANT_Methodref_info常量的索引谣光。在本例中,它的作用是調(diào)用PrintStream對(duì)象的println方法捡絮。
invokevirtual指令要求將調(diào)用目標(biāo)方法所需要的參數(shù)壓入棧頂瑞侮,除靜態(tài)方法、類(lèi)初始化方法<clinit>之外越妈,每個(gè)類(lèi)的成員方法以及類(lèi)的實(shí)例初始化方法<init>的第一個(gè)參數(shù)都是this引用钮糖,在java代碼中不需要傳遞,由編譯器編譯后生成且叁。
在本例中invokevirtual指令執(zhí)行之前,操作數(shù)棧必須存在一個(gè)System.out對(duì)象的引用展氓,和println方法所需的參數(shù),并且順序是嚴(yán)格要求的,正是前面getstatic执庐、ldc兩條指令執(zhí)行的結(jié)果。invokevirtual指令執(zhí)行完成后操作棧的變化如圖
1.讀寫(xiě)局部變量表與操作數(shù)棧
讀寫(xiě)局部變量表與操作數(shù)棧就是將局部變量push進(jìn)操作數(shù)棧與將操作數(shù)棧的棧頂元素存儲(chǔ)到局部變量表的操作。
將局部變量表中的元素放入操作數(shù)棧只能放入棧頂躏结,而將操作數(shù)棧的棧頂元素存到局部變量表是可以指定存到局部變量表的位置的黄橘,這個(gè)過(guò)程其實(shí)就是給局部變量賦值。
與匯編語(yǔ)言有相似之處就是字節(jié)碼指令不能直接將局部變量表的某個(gè)元素賦值給局部變量表的另一個(gè)元素,必須通過(guò)操作數(shù)棧完成椰于。這也是為什么說(shuō)字節(jié)碼指令集是基于棧的指令集。
局部變量表的大小與操作數(shù)棧的深度是在Java代碼編譯成class字節(jié)碼文件時(shí)就已經(jīng)確定,使用javap -v命令可以查看當(dāng)前class文件中每個(gè)方法的操作數(shù)棧深度與局部變量表大小竹挡。
以一個(gè)給局部變量賦值的例子理解讀寫(xiě)操作數(shù)棧與局部變量表宝泵,如代碼
public static void main(String[] args) {
int a = 10, b = 20;
int c = b;
b = a;
}
對(duì)應(yīng)的字節(jié)碼 (javap -v classname.class)
局部變量表的大小為4,操作數(shù)棧的大小是1椰弊。局部變量表的每個(gè)Slot分別用于存儲(chǔ)main方法中類(lèi)型為String數(shù)組的參數(shù)的引用,以及變量a清焕、b、c的值。
為什么局部變量表的大小為4,操作數(shù)棧的大小只是1呢挂签?我們帶著這個(gè)疑問(wèn)分析這些字節(jié)碼指令的執(zhí)行過(guò)程戏售。
通過(guò)javap查看字節(jié)碼,我們發(fā)現(xiàn)些己,在字節(jié)碼指令的前面都會(huì)標(biāo)有數(shù)字炉奴,如代碼編譯后的字節(jié)碼所示往堡。這些數(shù)字是每條指令在Code屬性中code[]數(shù)組的索引穆咐,也可稱為下標(biāo)或者偏移量。把這些指令的索引連在一起看心傀,發(fā)現(xiàn)不是連續(xù)的种呐,這是因?yàn)橛行┲噶钚枰僮鲾?shù)汁讼,在需要操作數(shù)的指令后面會(huì)存儲(chǔ)該指令執(zhí)行所需的操作數(shù)戈擒,所以指令前面的數(shù)字不是連續(xù)的。
現(xiàn)在我們分析代碼編譯后的字節(jié)碼指令的執(zhí)行過(guò)程柑土。偏移量為0的指令為bipush指令蜀肘,該指令是將一個(gè)立即數(shù)10放入操作數(shù)棧頂。該指令執(zhí)行完后稽屏,操作數(shù)棧與局部變量表的變化如圖
偏移量為2的指令是istore_1扮宠,該指令是將當(dāng)前操作數(shù)棧頂?shù)脑卮鎯?chǔ)到局部變量表索引為1的Slot(第二個(gè)Slot)收捣。該指令執(zhí)行完成后童漩,局部變量表索引為1的Slot存儲(chǔ)整數(shù)10,操作數(shù)棧頂?shù)脑匾呀?jīng)出棧搁胆,此時(shí)操作數(shù)棧為空。
Java虛擬機(jī)執(zhí)行字節(jié)碼指令并不關(guān)心局部變量表索引為1的元素在源碼中叫什么名字吗垮。那我們?cè)趺粗肋@個(gè)位置是局部變量a狼牺、b拂到、c的哪個(gè)呢?這就需要通過(guò)查看LocalVariableTable屬性了祝蝠。使用javap命令輸出此例子的LocalVariableTable屬性如下。
第一行:局部變量的作用范圍為[0,11)燕鸽,使用局部變量表中的第一個(gè)Slot存儲(chǔ)沟娱,該局部變量的名稱為“args”蓖扑,變量的類(lèi)型簽名為“[Ljava/lang/String”拆宛;
第二行:局部變量的作用范圍為[3,11)贡这,使用局部變量表中的第二個(gè)Slot存儲(chǔ),該局部變量的名稱為“a”证芭,類(lèi)型簽名為“I”;
第三行:局部變量的作用范圍為[6,11)没龙,使用局部變量表中的第三個(gè)Slot存儲(chǔ)邻辉,該局部變量的名稱為“b”吱瘩,類(lèi)型簽名為“I”;
第四行:局部變量的作用范圍為[8,11),使用局部變量表中的第四個(gè)Slot存儲(chǔ)买置,該局部變量的名稱為“c”惨好,類(lèi)型簽名為“I”绰精。
偏移量為3的字節(jié)碼指令為bipush指令,該指令的作用是將立即數(shù)20放入操作數(shù)棧頂萨蚕。該指令執(zhí)行完成后庆猫,局部變量a的值還是10构罗,操作數(shù)棧頂存儲(chǔ)立即數(shù)20召边。
偏移量為5的字節(jié)碼指令為istore_2铺呵,該指令不需要操作數(shù),作用是將當(dāng)前操作數(shù)棧的棧頂元素存儲(chǔ)到局部變量表索引為2的Slot隧熙。該指令執(zhí)行完成后片挂,a=10,b=20,操作數(shù)棧頂?shù)脑爻鰲Q缏簦僮鲾?shù)棧為空滋将。
偏移量為6的字節(jié)碼指令為aload_2,該指令不需要操作數(shù)症昏,作用是將局部變量表索引為2的元素放入操作數(shù)棧的棧頂随闽。該指令執(zhí)行完成后,a=10肝谭,b=20掘宪,操作數(shù)棧的棧頂存儲(chǔ)整數(shù)20。
偏移量為7的字節(jié)碼指令為istore_3攘烛,該指令的作用是將當(dāng)前操作數(shù)棧的棧頂元素存儲(chǔ)到局部變量表索引為3的Slot魏滚。偏移量為6和7的兩條指令完成將局部變量b賦值給局部變量c。該指令執(zhí)行完成后坟漱,a=10鼠次,b=20,c=20芋齿,操作數(shù)棧頂元素出棧腥寇,操作數(shù)棧為空
偏移量為8和9的兩條字節(jié)碼指令分別為iload_1和istore_2,這兩條字節(jié)碼指令的作用是完成局部變量a賦值給局部變量b的操作觅捆,這兩條指令執(zhí)行完成后赦役,局部變量與操作數(shù)棧的變化如圖
從整個(gè)方法的字節(jié)碼指令執(zhí)行過(guò)程來(lái)看,該方法執(zhí)行所需要占用操作數(shù)棧的Slot最多只有一個(gè)栅炒,因此該方法的操作數(shù)棧的大小被編譯器設(shè)置為1掂摔,不浪費(fèi)任何空間。而方法參數(shù)args和方法體內(nèi)聲明的局部變量a赢赊、b乙漓、c它們的作用域是整個(gè)方法,因此需要為args释移、a叭披、b、c都分配一個(gè)局部變量槽位秀鞭,局部變量表的大小被編譯器設(shè)置為4趋观。
我們通過(guò)這個(gè)例子了解了局部變量表和操作數(shù)棧的讀寫(xiě)扛禽,其中iload_xx指令就是將局部變量表的元素放入棧頂锋边,istore_xx指令就是將當(dāng)前操作數(shù)棧的棧頂元素存儲(chǔ)到局部變量表。xx是局部變量表的索引编曼,局部變量表是一個(gè)數(shù)組豆巨,需要通過(guò)索引訪問(wèn)數(shù)組中的元素。iload_xx和istore_xx對(duì)應(yīng)的字節(jié)碼指令如表
iload_xx和istore_xx只能訪問(wèn)局部變量表索引為0到3的元素掐场,那假如局部變量表的長(zhǎng)度超過(guò)4呢往扔,沒(méi)有iload_4指令贩猎?是的,沒(méi)有iload_4指令萍膛,只能使用iload和istore指令吭服。
其實(shí)不管訪問(wèn)局部變量表的哪個(gè)位置,都可以通過(guò)iload和istore指令訪問(wèn)蝗罗,那為什么還要iload_xx和istore_xx指令呢艇棕。因?yàn)閕load和istore指令需要操作數(shù),而iload_xx和istore_xx不需要操作數(shù)串塑,在編譯后能減少Code屬性的code[]字節(jié)數(shù)組的大小沼琉,而且大多數(shù)方法都不會(huì)超過(guò)3個(gè)參數(shù)。因?yàn)榉庆o態(tài)方法的局部變量表的下標(biāo)0用于保存this引用桩匪,所以是4減1個(gè)參數(shù)打瘪。
例子中的iload_xx指令和istore_xx指令只能操作Java中int類(lèi)型的變量,與之對(duì)應(yīng)的還有操作float類(lèi)型的fload_xx和fstore_xx指令傻昙,操作long類(lèi)型的lload_xx和lstore_xx指令闺骚,操作double類(lèi)型的dload_xx和dstore_xx指令,以及操作引用類(lèi)型的aload_xx和astore_xx指令屋匕,還有fload葛碧、lload、dload过吻、aload指令进泼。
例子中還用到了bipush指令。bipush用于將一個(gè)int型的立即數(shù)放入操作數(shù)棧的棧頂纤虽,該指令屬于操作常量與立即數(shù)入棧一類(lèi)的指令乳绕。除bipush之外還有將null放入操作數(shù)棧棧頂?shù)膇const_null指令、將常量池中的常量值放入操作數(shù)棧頂?shù)闹噶頻dc逼纸。還有iconst_xx指令洋措,xx可取值為-1到5,作用是將-1~5的立即數(shù)放入操作數(shù)棧頂杰刽。還有fconst_xx菠发、dconst_xx、lconst_xx贺嫂,xx代表0或1滓鸠,這些指令分別是將立即數(shù)1、2作為浮點(diǎn)數(shù)或者雙精度浮點(diǎn)數(shù)第喳、長(zhǎng)整型放入操作數(shù)棧頂糜俗,不過(guò)這幾條指令不常用。
在使用將立即數(shù)放入操作數(shù)棧棧頂?shù)倪@類(lèi)指令時(shí),如果立即數(shù)大于等于-1且小于等于5悠抹,可使用對(duì)應(yīng)的iconst_xx指令珠月,如果立即數(shù)超過(guò)5,只能使用bipush指令楔敌。這也是很多人第一次接觸字節(jié)碼指令時(shí)很是不理解的啤挎,為什么int a=3與int a=10反編譯后字節(jié)碼指令會(huì)不同的原因。
2. 基于對(duì)象的操作
Java語(yǔ)言是一門(mén)純面向?qū)ο蟮木幊陶Z(yǔ)言卵凑,除訪問(wèn)靜態(tài)方法和靜態(tài)變量以及類(lèi)的初始化方法外侵浸,幾乎都是基于對(duì)象的操作,如調(diào)用對(duì)象的方法氛谜,在方法中訪問(wèn)對(duì)象自身的字段掏觉。這節(jié)內(nèi)容我們將學(xué)習(xí)new一個(gè)對(duì)象在字節(jié)碼層面的實(shí)現(xiàn)、調(diào)用對(duì)象的set/get方法在字節(jié)碼的實(shí)現(xiàn)值漫、使用this獲取字段或?yàn)樽侄钨x值的字節(jié)碼實(shí)現(xiàn)澳腹、調(diào)用父類(lèi)方法的字節(jié)碼的實(shí)現(xiàn)。
public static void main(String[] args) {
UserService service = new UserService();
User user = service.getUser();
String name = user.getName();
System.out.println(name);
}
main方法中創(chuàng)建了一個(gè)UserService對(duì)象杨何,并調(diào)用UserService對(duì)象的getUser方法獲取一個(gè)User對(duì)象酱塔,調(diào)用User對(duì)象的getName方法獲取該對(duì)象的name字段的值。使用javap命令輸出這段代碼的字節(jié)碼如下危虱。
我們將整個(gè)方法的字節(jié)碼拆分成幾個(gè)部分來(lái)理解羊娃。第一部分是創(chuàng)建UserService對(duì)象,即“UserService service = new UserService();”埃跷,對(duì)應(yīng)的字節(jié)碼如下:
1蕊玷、偏移量為0的字節(jié)碼指令是new指令,操作碼為0xBB弥雹,該指令需要一個(gè)操作數(shù)垃帅,該操作數(shù)占兩個(gè)字節(jié),所以下一條指令的下標(biāo)為3剪勿。該操作數(shù)的值為常量池中某個(gè)CONSTANT_Class_info常量的索引贸诚,#2常量表示UserService類(lèi)。CONSTANT_Class_info結(jié)構(gòu)的name_index存儲(chǔ)的是常量池中某個(gè)CONSTANT_Utf8_info常量的索引厕吉,該CONSTANT_Utf8_info常量存儲(chǔ)的就是UserService類(lèi)的類(lèi)名酱固。該指令執(zhí)行會(huì)創(chuàng)建一個(gè)UserService類(lèi)型的對(duì)象,并且指令的返回值為對(duì)象的引用头朱,保存在操作數(shù)棧的棧頂运悲。此時(shí)操作數(shù)棧的變化如圖
2、偏移量為3的字節(jié)碼指令是dup指令髓窜,操作碼為0x59扇苞,該指令不需要操作數(shù),因此下一條指令的下標(biāo)為4寄纵。該指令的功能是復(fù)制當(dāng)前棧頂一個(gè)Slot的數(shù)據(jù)鳖敷,復(fù)制后的數(shù)據(jù)存儲(chǔ)在操作數(shù)棧的棧頂,此時(shí)操作數(shù)棧的變化如圖
3程拭、偏移量為4的字節(jié)碼指令是invokespecial定踱,這是一條方法調(diào)用指令,操作碼為0xB7恃鞋,該指令需要一個(gè)操作數(shù)崖媚,操作數(shù)的值是常量池中某個(gè)CONSTANT_Methodref_info常量的索引,#3常量表示UserService的<init>方法恤浪。
CONSTANT_Methodref_info常量的class_index項(xiàng)的值是常量池中某個(gè)CONSTANT_Class_info常量的索引畅哑,表示一個(gè)類(lèi),在本例中表示UserService類(lèi)水由。CONSTANT_Methodref_info常量的name_and_type_index項(xiàng)的值是常量池中某個(gè)CONSTANT_NameAndType_info常量的索引荠呐,這個(gè)常量表示當(dāng)前方法的名稱和方法的描述符,在本例中表示的名稱為<init>砂客,方法描述符為”()V”泥张。所以invokespecial指令是調(diào)用UserService類(lèi)的實(shí)例初始化方法<init>。
由于UserService的實(shí)例初始化方法<init>需要隱式傳遞一個(gè)this引用參數(shù)鞠值,隱式傳遞指的是Java代碼中方法沒(méi)有聲明此參數(shù)媚创。因此需要在該指令執(zhí)行之前,操作數(shù)棧頂存放的是一個(gè)UserService類(lèi)型對(duì)象的引用彤恶,即調(diào)用UserService類(lèi)的實(shí)例初始化方法<init>時(shí)將this引用傳遞給<init>方法钞钙,該this參數(shù)會(huì)存儲(chǔ)在<init>方法的局部變量表索引為0的Slot。<init>方法沒(méi)有返回值声离,因此該指令執(zhí)行完后操作數(shù)棧的變化如圖
4歇竟、偏移量為7的指令是astore_1,該指令是將當(dāng)前棧頂?shù)囊妙?lèi)型的數(shù)據(jù)存儲(chǔ)到局部變量表索引為1的Slot。如果查看LocalVariableTable赂蕴,局部變量表下標(biāo)1的位置存儲(chǔ)的是局部變量service逮京。該指令執(zhí)行完成后,操作數(shù)棧與局部變量表的變化如圖
由此可知盅安,new一個(gè)對(duì)象需要四條字節(jié)碼指令,先創(chuàng)建對(duì)象存放在棧頂世囊,然后將棧頂存放的對(duì)象復(fù)制一份别瞭,用于調(diào)用類(lèi)的實(shí)例初始化方法,最后還是將new指令創(chuàng)建出來(lái)的那份賦值給局部變量或者字段株憾。這也就很好理解并發(fā)編程中蝙寨,為什么單例要使用雙重檢測(cè)晒衩,因?yàn)閚ew一個(gè)對(duì)象在字節(jié)碼層面看并不是一個(gè)原子操作。
現(xiàn)在我們來(lái)分析第二部分字節(jié)碼墙歪。第二部分對(duì)應(yīng)的Java源代碼是“User user =service.getUser()”听系,即調(diào)用剛剛創(chuàng)建的UserService對(duì)象的getUser方法獲取一個(gè)user對(duì)象。第二部分對(duì)應(yīng)的字節(jié)碼如下虹菲。
1靠胜、偏移量為8的字節(jié)碼指令是aload_1,該指令將局部變量表索引為1的元素放到操作數(shù)棧頂毕源,即將第一部分字節(jié)碼創(chuàng)建出來(lái)的UserService對(duì)象的引用放入操作數(shù)棧頂浪漠。該指令執(zhí)行完成后操作數(shù)棧的變化如圖
2、偏移量為9的字節(jié)碼指令是invokevirtual霎褐,操作碼為0xB6址愿,該指令也是方法調(diào)用指令,需要一個(gè)操作數(shù)冻璃,該操作數(shù)必須是常量池中某個(gè)CONSTANT_Methodref_info常量的索引必盖,在本例中#4指向的常量表示UserService的getUser方法。調(diào)用該方法只需要一個(gè)隱式參數(shù)俱饿,因此需要將一個(gè)UserService對(duì)象的引用放入操作數(shù)棧頂歌粥。UserService的getUser方法有一個(gè)返回值,返回值的類(lèi)型為引用類(lèi)型拍埠,即返回一個(gè)User實(shí)例的引用失驶。該指令執(zhí)行完成后操作數(shù)棧的變化如圖
3、偏移量為12的指令是astore_2指令枣购,該指令是將當(dāng)前操作數(shù)棧頂?shù)脑豒ser對(duì)象的引用存儲(chǔ)到局部變量表索引為2的Slot嬉探。該指令執(zhí)行完成后,此時(shí)的操作數(shù)棧又恢復(fù)到未使用狀態(tài)棉圈。
第三部分字節(jié)碼對(duì)應(yīng)的Java源代碼是“String name = user.getName()”涩堤,即調(diào)用User對(duì)象的getName方法。這部分的字節(jié)碼指令如下分瘾。
1胎围、偏移量為13的字節(jié)碼指令是aload_2,在例子中德召,是將局部變量表索引為2的Slot存儲(chǔ)的User對(duì)象的引用推送至操作數(shù)棧的棧頂白魂。
2、偏移量為14的字節(jié)碼指令是invokevirtual上岗,操作數(shù)#5執(zhí)向常量池中索引為5的CONSTANT_Methodref_info常量福荸,表示User類(lèi)的getName方法。User的getName方法返回值類(lèi)型為String肴掷,該指令執(zhí)行完成后敬锐,操作數(shù)棧頂?shù)脑厥莋etName方法返回的String對(duì)象的引用背传。
3、偏移量為17的字節(jié)碼指令是astore_3台夺,該指令是將上一步調(diào)用User對(duì)象getName方法的返回值String類(lèi)型的引用存儲(chǔ)到局部變量表索引為3的Slot径玖,也就是給局部變量name賦值。
分析完字節(jié)碼各指令的執(zhí)行過(guò)程后谒养,我們發(fā)現(xiàn),get明郭、set方法與普通的成員方法調(diào)用并沒(méi)有什么區(qū)別买窟。我們也認(rèn)識(shí)了新的指令new、dup薯定、invokespecial和invokevirtual始绍。new指令用于創(chuàng)建對(duì)象,dup指令用于復(fù)制當(dāng)前棧頂元素话侄,invokespecial和invokevirtual指令用于方法的調(diào)用亏推。
讀寫(xiě)this的字段
讀寫(xiě)this字段最常見(jiàn)的還是get、set方法年堆,而在web項(xiàng)目中吞杭,我們經(jīng)常會(huì)在Service層注入Dao層的對(duì)象,調(diào)用Service的方法完成業(yè)務(wù)邏輯变丧,在Service的方法中會(huì)調(diào)用Dao層的方法芽狗,此時(shí)獲取Dao層對(duì)象通過(guò)”this.字段名”訪問(wèn)。
public class UserService extends BaseService {
private UserDao userDao = new UserDao();
public User findUser(String username) {
return userDao.getUserByName(username);
}
}
使用javap命令輸出UserService類(lèi)的findUser方法的字節(jié)碼如下痒蓬。
偏移量為0和1這兩條字節(jié)碼指令對(duì)應(yīng)的java代碼就是this.userDao童擎。首先使用aload_0將局部變量表索引為0的Slot存儲(chǔ)的值放入操作數(shù)棧頂,對(duì)于非靜態(tài)方法攻晒,局部變量表索引為0的Slot存儲(chǔ)的變量就是this引用顾复。接著使用getfield指令獲取this的userDao字段,getfield指令要求一個(gè)操作數(shù)鲁捏,操作數(shù)的值為常量池中某個(gè)CONSTANT_Fieldref_info常量的索引芯砸,本例中索引為4的常量表示userDao字段,字段的類(lèi)型描述符為“Lcom/wujiuye/asmbytecode/book/third/dao/UserDao;”给梅。該指令執(zhí)行完成后返回this.userDao乙嘀,存儲(chǔ)在操作數(shù)棧頂。
我們?cè)賮?lái)看一個(gè)給this.userDao賦值的例子
private UserDao userDao = new UserDao();
public void onInit() {
this.userDao = new UserDao();
}
偏移量為0的aload_0指令是將this引用放入操作數(shù)棧棧頂破喻。此時(shí)操作數(shù)棧的變化如圖
偏移量為1虎谢、4、5三條指令是創(chuàng)建一個(gè)UserDao對(duì)象曹质,這三條指令執(zhí)行完成后婴噩,操作數(shù)棧頂?shù)淖兓鐖D
putfield指令與getfield指令需要的操作數(shù)都是一樣擎场,偏移量為8的putfield指令是將當(dāng)前棧頂元素賦值給this.userDao字段。
實(shí)例初始化方法
實(shí)例初始化方法是在創(chuàng)建對(duì)象之后調(diào)用的几莽,Java代碼中使用new關(guān)鍵字創(chuàng)建一個(gè)對(duì)象迅办,編譯成字節(jié)碼后是通過(guò)兩條指令來(lái)完成的,第一條是new指令章蚣,第二條是方法調(diào)用指令站欺,即調(diào)用類(lèi)的實(shí)例初始化方法<init>。我們還是以UserService為例纤垂,看UserService的init方法的字節(jié)碼
可以看出矾策,我們?cè)诰帉?xiě)Java代碼給UserService的userDao字段直接賦值,編譯器將賦值操作放到<init>方法了峭沦。對(duì)應(yīng)圖中code部分偏移量為4到12的字節(jié)碼指令贾虽。
類(lèi)的實(shí)例初始化方法<init>是由編譯器生成的,對(duì)象的字段初始化賦值也被編譯進(jìn)該方法中完成吼鱼,構(gòu)造方法也是編譯進(jìn)該方法蓬豁。
<init>方法中要求必須調(diào)用父類(lèi)的<init>方法。如圖所示菇肃,編譯器生成的<init>方法會(huì)先調(diào)用父類(lèi)的<init>方法地粪,這是我們使用ASM框架操作字節(jié)碼生成<init>方法時(shí)需要注意的,否則類(lèi)加載驗(yàn)證字節(jié)碼階段會(huì)通不過(guò)琐谤。
如果同時(shí)存在多個(gè)構(gòu)造方法驶忌,比如無(wú)參構(gòu)造方法和帶參數(shù)構(gòu)造方法,那么編譯器是如何生成<init>方法的呢笑跛?我們修改下UserService的代碼付魔,添加一個(gè)帶參構(gòu)造方法。
public UserService(UserDao userDao) {
this.userDao = userDao;
}
編譯器生成的構(gòu)造方法依然會(huì)先調(diào)用父類(lèi)的實(shí)例初始化方法飞蹂,也會(huì)將字段的初始化賦值編譯進(jìn)該方法几苍,最后才是將方法傳入的參數(shù)參數(shù)賦值給this對(duì)象的字段。那么如果在帶參數(shù)的構(gòu)建方法中調(diào)用”this()”呢陈哑?
public UserService(UserDao userDao) {
this();
this.userDao = userDao;
}
編譯器生成的帶參數(shù)的實(shí)例初始化方法不再調(diào)用父類(lèi)的實(shí)例初始化方法妻坝,因?yàn)樵摲椒〞?huì)調(diào)用本類(lèi)的無(wú)參數(shù)實(shí)例初始化方法,在本類(lèi)的無(wú)參實(shí)例初始化方法中已經(jīng)調(diào)用了父類(lèi)的無(wú)參實(shí)例初始化方法惊窖。
調(diào)用父類(lèi)的方法
在Java代碼中刽宪,如果在重寫(xiě)的方法中調(diào)用父類(lèi)的方法需要使用super關(guān)鍵字,未被重寫(xiě)的子類(lèi)可訪問(wèn)的方法則不需要使用super關(guān)鍵字界酒,這其實(shí)是由編譯器完成了圣拄,使用super關(guān)鍵字調(diào)用的方法編譯后的字節(jié)碼指令,其操作數(shù)指向父類(lèi)的方法引用毁欣,即調(diào)用的方法的CONSTANT_Methodref_info常量的class_index指向的CONSTANT_Class_info常量表示的是父類(lèi)庇谆。我們來(lái)看一個(gè)例子
public abstract class BaseService {
public void testInvokeSuperMethod() {
System.out.println("BaseService testInvokeSuperMethod....");
}
}
public class UserService extends BaseService {
@Override
public void testInvokeSuperMethod() {
super.testInvokeSuperMethod();
}
}
UserService繼承BaseService并重寫(xiě)了父類(lèi)的testInvokeSuperMethod方法岳掐,在重寫(xiě)的方法中通過(guò)super關(guān)鍵字調(diào)用父類(lèi)的方法。使用javap命令輸出testInvokeSuperMethod方法的字節(jié)碼如下饭耳。
從字節(jié)碼中可以看出串述,super關(guān)鍵字編譯后就不存在了,調(diào)用父類(lèi)的方法依然是使用this引用寞肖,aload_0是將局部變量表索引為0的元素放入操作數(shù)棧纲酗,然后執(zhí)行invokespecial指令調(diào)用方法。而局部變量表索引為0的Slot存儲(chǔ)的正是this引用新蟆。
那么虛擬機(jī)是怎么區(qū)分是調(diào)用父類(lèi)的方法的觅赊?答案就在方法描述符,super關(guān)鍵字調(diào)用的方法栅葡,編譯器會(huì)將invokespecial指令所需的操作數(shù)指向表示父類(lèi)方法的CONSTANT_Methodref_info常量茉兰。本例中尤泽,invokespecial指令后面跟的操作數(shù)是7欣簇,正是表示BaseService類(lèi)的testInvokeSuperMethod方法。
3. 訪問(wèn)靜態(tài)字段與靜態(tài)方法
與非靜態(tài)方法的調(diào)用和非靜態(tài)字段的訪問(wèn)不同坯约,獲取靜態(tài)字段熊咽、修改靜態(tài)字段、調(diào)用靜態(tài)方法不需要一個(gè)該類(lèi)型的對(duì)象引用作為隱式參數(shù)闹丐,且靜態(tài)方法的局部變量表不會(huì)存儲(chǔ)this引用横殴。
靜態(tài)字段的初始賦值由編譯器編譯后在類(lèi)初始化方法<clinit>中生成賦值的字節(jié)碼指令,而被聲明為final的靜態(tài)字段初始賦值則在類(lèi)加載的準(zhǔn)備階段賦值卿拴。
讀寫(xiě)靜態(tài)字段的字節(jié)碼指令是getstatic與putstatic衫仑,這兩條指令都要求一個(gè)操作數(shù),操作數(shù)的值為常量池中某個(gè)CONSTANT_Fieldref_info常量的索引堕花。getstatic指令的操作碼為0xB2文狱,putstatic指令的操作碼為0xB3。
讀寫(xiě)靜態(tài)字段的例子如代碼
public class StaticFieldMain {
static String name;
public static void main(String[] args) {
name = "congzhizhi";
System.out.println(name);
}
}
偏移量為0和2的字節(jié)碼指令完成為靜態(tài)字段name賦值缘挽,先使用ldc字節(jié)碼指令將putstatic指令所需要的參數(shù)放入操作數(shù)棧頂瞄崇,putstatic指令將棧頂?shù)脑刭x值給類(lèi)的靜態(tài)字段。
調(diào)用靜態(tài)方法的例子如代碼
public class StaticMethodMain {
static void show(String msg){
System.out.println(msg);
}
public static void main(String[] args) {
StaticMethodMain.show("hello word!");
}
}
例子中壕曼,在main方法調(diào)用show靜態(tài)方法苏研,調(diào)用show方法需要傳遞一個(gè)參數(shù),在show方法中打印main方法傳遞的參數(shù)腮郊。對(duì)應(yīng)的字節(jié)碼如下摹蘑。
main方法中,偏移量為0和2的字節(jié)碼指令完成調(diào)用show方法轧飞,ldc指令將調(diào)用show方法所需的參數(shù)放入操作數(shù)棧的棧頂纹蝴。方法需要多少個(gè)參數(shù)就將多少個(gè)參數(shù)放入操作數(shù)棧頂庄萎,如果傳null則使用aconst_null指令,aconst_null指令的操作碼為0x01塘安。調(diào)用靜態(tài)方法的指令是invokestatic糠涛,指令的操作碼為0xB8,該指令需要一個(gè)操作數(shù)兼犯,操作數(shù)的值必須是常量池中某個(gè)CONSTANT_Methodref_info常量的索引忍捡。在show方法中,偏移量為3的aload_0指令獲取到的局部變量不再是this切黔,而是方法的第一個(gè)參數(shù)砸脊。
4. 調(diào)用方法的四條指令
在Java字節(jié)碼指令集中有四條調(diào)用方法的指令,嚴(yán)格來(lái)說(shuō)是五條纬霞,在JDK1.7中加入了invokedynamic指令凌埂。常用的四條方法調(diào)用指令如表
這四條方法調(diào)用指令都需要一個(gè)執(zhí)向常量池中某個(gè)CONSTANT_Methodref_info常量的操作數(shù),即告訴jvm诗芜,該指令調(diào)用的是哪個(gè)類(lèi)的哪個(gè)方法瞳抓。除invokestatic指令外,其余指令都至少需要一個(gè)參數(shù)伏恐,這個(gè)參數(shù)就是隱式參數(shù)this孩哑。
其中invokestatic指令用于調(diào)用一個(gè)靜態(tài)方法,只要調(diào)用的方法是靜態(tài)方法就必須要使用這條指令翠桦。invokeinterface指令用于調(diào)用接口方法横蜒,運(yùn)行時(shí)再根據(jù)對(duì)象的類(lèi)型找出一個(gè)實(shí)現(xiàn)該接口方法的適合方法進(jìn)行調(diào)用。invokespecial指令用于調(diào)用實(shí)例初始化方法销凑、私有方法和父類(lèi)的子類(lèi)可訪問(wèn)的方法丛晌。invokevirtual指令用于調(diào)用對(duì)象的實(shí)例方法,根據(jù)對(duì)象的實(shí)際類(lèi)型進(jìn)行分派斗幼。
5. 不同類(lèi)型返回值對(duì)應(yīng)的指令
與讀寫(xiě)局部變量表和操作數(shù)棧一樣澎蛛,方法返回指令也對(duì)應(yīng)有多條指令,每種基本數(shù)據(jù)類(lèi)型對(duì)應(yīng)一條指令孟岛,引用類(lèi)型對(duì)應(yīng)areturn指令瓶竭,如表
return指令用于無(wú)返回值方法,在java代碼中渠羞,void方法我們可能不會(huì)寫(xiě)return斤贰,但編譯器會(huì)自動(dòng)加上return指令。以返回值為int次询、long基本數(shù)據(jù)類(lèi)型為例荧恍,對(duì)應(yīng)java代碼如下。
public static int getInt(){
return 1000000000;
}
public static long getLong(){
return 1000000000000000000L;
}
驗(yàn)證返回值類(lèi)型為引用類(lèi)型時(shí)使用的返回指令為areturn,代碼如下送巡。
public static User getObject() {
return new User();
}
6. 條件分支語(yǔ)句的實(shí)現(xiàn)
Java語(yǔ)言提供的條件分支語(yǔ)句包含if語(yǔ)句摹菠、switch語(yǔ)句、三目運(yùn)算符骗爆,這些條件語(yǔ)句是如何通過(guò)字節(jié)碼實(shí)現(xiàn)的呢次氨?
if語(yǔ)句
使用Java語(yǔ)言實(shí)現(xiàn)的if語(yǔ)句如代碼
public int ifFunc(int type) {
if (type == 1) {
} else if (type == 2) {
return 1000;
} else {
return 0;
}
return 100;
}
使用javap命令輸出ifFunc方法的字節(jié)碼如下。
偏移量為0摘投、1煮寡、2三條字節(jié)碼指令完成第一個(gè)if語(yǔ)句的判斷。iload_1將參數(shù)type的值放入操作數(shù)棧頂犀呼,由于是非靜態(tài)方法幸撕,所示局部變量表索引為0的Slot存儲(chǔ)的是this引用,因此局部變量表索引為1的Slot存儲(chǔ)的才是方法的第一個(gè)參數(shù)外臂。iconst_1指令將立即數(shù)1放入操作數(shù)棧頂坐儿。if_icmpne指令完成操作數(shù)棧頂兩個(gè)整數(shù)的比較,該指令的操作碼為0xA0宋光,指令執(zhí)行需要一個(gè)操作數(shù)貌矿,操作數(shù)是當(dāng)前方法某條字節(jié)碼指令的偏移量。當(dāng)棧頂?shù)膬蓚€(gè)int類(lèi)型的元素不相等時(shí)跃须,跳轉(zhuǎn)到操作數(shù)指向的字節(jié)碼指令站叼。
if_icmpne字節(jié)碼指令是判斷兩個(gè)值不相等才跳轉(zhuǎn)娃兽,這與java代碼剛好相反菇民。在java代碼中,if左右兩個(gè)元素相等才執(zhí)行if體內(nèi)的代碼投储,而編譯后字節(jié)碼指令按if與else if第练、else的編寫(xiě)順序生成,當(dāng)if左右兩個(gè)元素相等時(shí)繼續(xù)往下執(zhí)行便是對(duì)應(yīng)java語(yǔ)言中的if語(yǔ)句的代碼塊玛荞,因此字節(jié)碼層面會(huì)看到相反的條件比較跳轉(zhuǎn)娇掏。
偏移量為8、9勋眯、10的三條字節(jié)碼指令也是完成比較跳轉(zhuǎn)的操作婴梧,最后一個(gè)else從偏移量為17的字節(jié)碼指令開(kāi)始,如果else代碼塊中沒(méi)有返回指令客蹋,那么會(huì)繼續(xù)往下執(zhí)行塞蹭。如果第一個(gè)if中沒(méi)有返回指令呢?如代碼清單
public int ifFunc2(int type) {
if (type == 1) {
type = 2;
} else {
type = 3;
}
return type;
}
如字節(jié)碼所示讶坯,編譯器在if_icmpne指令后面為局部變量type賦值后番电,使用一條goto指令跳轉(zhuǎn)到else結(jié)束的后面的第一條字節(jié)碼指令。
所以,當(dāng)if或者else if的代碼塊中沒(méi)有return指令時(shí)漱办,編譯器會(huì)為其添加一條goto指令用于跳出if條件分支語(yǔ)句这刷。goto指令是無(wú)條件跳轉(zhuǎn)指令,操作碼為0xA7娩井,操作數(shù)是當(dāng)前方法的某條字節(jié)碼指令的偏移量暇屋,本例中,goto指令的操作碼是12洞辣,表示跳轉(zhuǎn)到偏移量為12的字節(jié)碼指令率碾,偏移量為12的字節(jié)碼指令是iload_1,所以goto指令之后將會(huì)指向該指令屋彪。
if_icmpne指令用于兩個(gè)int類(lèi)型值比較所宰,不相等才跳轉(zhuǎn),更多比較跳轉(zhuǎn)指令如表
與0比較的跳轉(zhuǎn)指令如表
switch語(yǔ)句
使用Java語(yǔ)言實(shí)現(xiàn)的switch語(yǔ)句如代碼
public int switchFunc(int stat) {
int a = 0;
switch (stat) {
case 5:
a = 0;
break;
case 6:
case 8:
a = 1;
break;
}
return a;
}
與if語(yǔ)句一樣的是畜挥,switch代碼塊中的每個(gè)case代碼塊都是按順序編譯生成字節(jié)碼的仔粥,switch代碼塊中的所有字節(jié)碼都在tableswitch這條指令的后面。
tableswitch指令的操作碼為0xAA蟹但,該指令的操作數(shù)是不定長(zhǎng)的躯泰,每個(gè)操作數(shù)的長(zhǎng)度為四個(gè)字節(jié),編譯器會(huì)為case區(qū)間(本例中华糖,case最小值為5麦向,最大值為8,區(qū)間為[5,8])的每一個(gè)數(shù)字都生成一個(gè)case語(yǔ)句客叉,就是添加一個(gè)操作數(shù)诵竭,操作數(shù)存放下一條字節(jié)碼指令的相對(duì)偏移量,注意兼搏,是相對(duì)偏移量卵慰。以上面例子說(shuō)明,tableswitch指令對(duì)應(yīng)的字節(jié)碼為:
第一個(gè)字節(jié)0xAA是tableswitch指令的操作碼佛呻,后面每四個(gè)字節(jié)為一個(gè)操作數(shù)裳朋。前面四個(gè)字節(jié)0x00000024轉(zhuǎn)為10進(jìn)制是36,由于tableswitch指令的偏移量為3吓著,因此該操作數(shù)表示匹配default時(shí)跳轉(zhuǎn)到偏移量為39的字節(jié)碼指令鲤嫡。緊隨其后的是0x00000005與0x00000008,這兩個(gè)數(shù)代表表格的區(qū)間绑莺,從5到8暖眼,也就是case 5到case 8,雖然我們代碼中沒(méi)有case 7紊撕,編譯器還是為我們生成了罢荡。后面的0x0000001d、0x00000022、0x00000024区赵、0x00000022分別+3得到的結(jié)果就是case 5到8分別跳轉(zhuǎn)到的目標(biāo)字節(jié)碼指令的絕對(duì)偏移量惭缰。
從前面的例子我們可以看出,tableswitch指令生成的字節(jié)碼占用的空間很大笼才,而且當(dāng)case的值不連續(xù)時(shí)漱受,還會(huì)生成一些無(wú)用的映射。如果case的每個(gè)值都不連續(xù)呢骡送?如代碼
public int switch2Func(int stat) {
int a = 0;
switch (stat) {
case 1:
a = 0;
break;
case 100:
a = 1;
break;
}
return a;
}
假設(shè)昂羡,編譯器將代碼清單3-43的switch語(yǔ)句生成tableswitch指令,那么這條指令將浪費(fèi)掉4乘以98的字節(jié)空間摔踱,如果再加個(gè)case 1000虐先,那么浪費(fèi)的空間更大。顯然派敷,這種情況下再使用tableswitch指令是不可取的蛹批。
正如你所看到的,編譯器使用lookupswitch指令替代了tableswitch指令篮愉。lookupswitch指令的操作碼為0xAB腐芍,與tableswitch指令一樣,該指令的操作數(shù)也是不定長(zhǎng)的试躏,每個(gè)操作數(shù)的長(zhǎng)度為四個(gè)字節(jié)猪勇,操作數(shù)存放的也是下一條字節(jié)碼指令的相對(duì)偏移量,注意颠蕴,還是相對(duì)偏移量泣刹。以上面例子說(shuō)明,lookupswitch指令對(duì)應(yīng)的字節(jié)碼為裁替。
第一個(gè)字節(jié)0xAB是lookupswitch指令的操作碼项玛,接著后面四個(gè)字節(jié)也是匹配default時(shí)跳轉(zhuǎn)的目標(biāo)指令相對(duì)當(dāng)前指令的偏移量貌笨,緊隨其后四個(gè)字節(jié)0x00000002代表后面跟隨多少個(gè)條件映射弱判,每八個(gè)字節(jié)為一個(gè)條件映射,前四個(gè)字節(jié)為匹配條件锥惋,后四個(gè)字節(jié)為條件匹配時(shí)跳轉(zhuǎn)的目標(biāo)字節(jié)碼指令的相對(duì)偏移量昌腰。0x00000001表示當(dāng)當(dāng)前操作數(shù)棧棧頂?shù)闹禐?時(shí),跳轉(zhuǎn)到相對(duì)偏移量為0x00000019的字節(jié)碼指令膀跌,0x00000019轉(zhuǎn)為10進(jìn)制是25遭商,加上當(dāng)前l(fā)ookupswitch指令的偏移量3等于28;0x00000064轉(zhuǎn)為十進(jìn)制為100捅伤,0x0000001E轉(zhuǎn)為十進(jìn)制加上3等于33劫流。
三目運(yùn)算符
三目運(yùn)算符也叫三元運(yùn)算符,這是由三個(gè)操作數(shù)組成的運(yùn)算符。
public int syFunc(boolean sex) {
return sex ? 1 : 0;
}
由于方法參數(shù)sex是boolean類(lèi)型祠汇,因此使用sex作為條件表達(dá)式編譯后會(huì)使用ifeq指令實(shí)現(xiàn)跳轉(zhuǎn)仍秤,即與0比較。當(dāng)前操作數(shù)棧頂元素的值等于0則跳轉(zhuǎn)可很,不等于0繼續(xù)往下執(zhí)行诗力。
三目運(yùn)算符的表達(dá)式為:<表達(dá)式1>?<表達(dá)式2>:<表達(dá)式3>我抠。因此三目運(yùn)算符也支持多層嵌套苇本,但實(shí)際開(kāi)發(fā)中不建議這么做,因?yàn)闀?huì)導(dǎo)致代碼能以理解菜拓。
7.循環(huán)語(yǔ)句的實(shí)現(xiàn)
Java語(yǔ)言提供的循環(huán)語(yǔ)句包括for瓣窄、while和do-while,由于do-while不常用纳鼎,因此本章不做介紹康栈。Java循環(huán)語(yǔ)句的底層字節(jié)碼實(shí)現(xiàn)實(shí)際上與條件分支語(yǔ)句的實(shí)現(xiàn)差不多,都是通過(guò)條件跳轉(zhuǎn)指令完成喷橙。
while循環(huán)
我們通過(guò)一個(gè)簡(jiǎn)單的while循環(huán)例子啥么,了解while循環(huán)在字節(jié)碼層面的實(shí)現(xiàn)。
public void whileDemo() {
int count = 10;
while (count > 0) {
count--;
}
}
偏移量為0的字節(jié)碼指令為bipush贰逾,該指令將立即數(shù)10放到操作數(shù)棧頂悬荣,接著使用istore_1指令將操作數(shù)棧棧頂?shù)?0存儲(chǔ)到局部變量表索引為1的Slot,也就是給局部變量count賦值疙剑。雖然只有一個(gè)局部變量氯迂,但因?yàn)樗饕秊?的Slot用來(lái)存儲(chǔ)this引用了,所以局部變量count存儲(chǔ)在局部變量表的索引為1的Slot言缤。
偏移量為3到10的字節(jié)碼指令實(shí)現(xiàn)while循環(huán)嚼蚀。iload_1將局部變量count的值放到操作數(shù)棧棧頂,接著使用ifle條件跳轉(zhuǎn)指令判斷棧頂?shù)脑厥欠裥∮诘扔?管挟,如果小于等于0則跳轉(zhuǎn)到偏移量為13的字節(jié)碼指令轿曙,也就是結(jié)束while循環(huán)。ifle后面跟的是while循環(huán)體中的代碼僻孝,iinc指令是將局部變量count減1导帝。while循環(huán)體結(jié)束處會(huì)加上一條goto指令,goto指令是無(wú)條件跳轉(zhuǎn)指令穿铆,本例中用于跳轉(zhuǎn)到偏移量為3的字節(jié)碼指令您单,直到ifle指令的條件成立才跳轉(zhuǎn)到return指令結(jié)束循環(huán)。
for循環(huán)
public int forDemo() {
int count = 0;
for(int i = 1; i <= 10; ++i) {
count += i;
}
return count;
}
其中荞雏,偏移量為0虐秦、1的兩條字節(jié)碼指令實(shí)現(xiàn)為局部變量count賦值為0平酿;偏移量為2、3的兩條字節(jié)碼指令實(shí)現(xiàn)為局部變量i賦值悦陋;偏移量為4染服、5、7的字節(jié)碼指令判斷局部變量i是否大于10叨恨,條件成立則跳轉(zhuǎn)到偏移量為20的字節(jié)碼指令執(zhí)行柳刮;偏移量為10、11痒钝、12秉颗、13這四條字節(jié)碼指令為局部變量count的值加1;偏移量為13的字節(jié)碼指令給局部變量i的值加1送矩;偏移量為17的字節(jié)碼指令告訴虛擬機(jī)下一條指令的偏移量為4蚕甥,即跳轉(zhuǎn)到偏移量為4的字節(jié)碼指令,而偏移量為4開(kāi)始的連續(xù)3條指令就是判斷局部變量i是否大于10的栋荸,這便是for循環(huán)的實(shí)現(xiàn)菇怀。
8.異常處理的實(shí)現(xiàn)
在Java代碼中,我們可通過(guò)try-catch-finally塊對(duì)異常進(jìn)行捕獲或處理晌块。其中catch塊可以有零個(gè)或多個(gè)憎蛤,finally塊可有可無(wú)仿野。如果catch有多個(gè)稍算,而第一個(gè)catch的異常的類(lèi)型是后面catch的異常的類(lèi)型的父類(lèi)虱痕,那么后面的catch塊不會(huì)起作用。那么我們?nèi)绾卧谧止?jié)碼層面實(shí)現(xiàn)try-catch-finally塊呢钝尸?
try-catch
我們來(lái)看一個(gè)簡(jiǎn)單的try-catch使用例子
public int tryCatchDemo() {
try {
int n = 100;
int m = 0;
return n / m;
} catch (ArithmeticException e) {
return -1;
}
}
異常表存儲(chǔ)在Code屬性中括享,異常表每項(xiàng)元素的結(jié)構(gòu)見(jiàn)第二章。tryCatchDemo方法的異常表只有一項(xiàng)珍促,該項(xiàng)的from铃辖、to、target存儲(chǔ)的是方法字節(jié)碼指令的偏移量猪叙,從from到to的字節(jié)碼對(duì)應(yīng)try代碼塊中的代碼娇斩,target指向的字節(jié)碼指令是catch代碼塊的開(kāi)始,type是該catch塊捕獲的異常沐悦。也就是說(shuō)成洗,在執(zhí)行偏移量為0到7的字節(jié)碼指令時(shí),如果拋出類(lèi)型為ArithmeticException的異常藏否,那么虛擬機(jī)將執(zhí)行偏移量為9開(kāi)始的字節(jié)碼指令。
在本例中充包,如果try代碼塊中拋出的不是ArithmeticException異常副签,虛擬機(jī)將結(jié)束當(dāng)前方法的執(zhí)行遥椿,將異常往上拋出。如果直到當(dāng)前線程的第一個(gè)方法都沒(méi)有遇到catch代碼塊處理這個(gè)異常淆储,那么當(dāng)前線程將會(huì)異常結(jié)束冠场,線程被虛擬機(jī)銷(xiāo)毀。
try-catch-finally
final語(yǔ)意是如何實(shí)現(xiàn)的本砰,為什么finally代碼塊的代碼總能被執(zhí)行到碴裙?我們來(lái)看一個(gè)例子,如代碼
public int tryCatchFinalDemo() {
try {
int n = 100;
int m = 0;
return n / m;
} catch (ArithmeticException e) {
return -1;
} finally {
System.out.println("finally");
}
}
先看異常表点额。異常表的第一項(xiàng)對(duì)應(yīng)tryCatchFinalDemo方法中的catch舔株,當(dāng)偏移量為0到9(不包括9)的字節(jié)碼指令在執(zhí)行過(guò)程中拋出異常時(shí),如果異常類(lèi)型為ArithmeticException則跳轉(zhuǎn)到偏移量為19的字節(jié)碼指令还棱,也就是執(zhí)行catch塊载慈。但后面的3項(xiàng)又是什么呢?
對(duì)照tryCatchFinalDemo方法編譯后的字節(jié)碼指令看珍手。偏移量為0到9的字節(jié)碼對(duì)應(yīng)try代碼塊中的Java代碼办铡,而19到22對(duì)應(yīng)catch塊中的Java代碼,32到42的字節(jié)碼指令對(duì)應(yīng)finally塊中的Java代碼琳要。偏移量為32的字節(jié)碼指令是將異常存儲(chǔ)到局部變量表索引為4的Slot寡具,這是因?yàn)樵趫?zhí)行finally塊中的代碼之前需要將當(dāng)前異常保存,以便于在執(zhí)行完finally塊中的代碼之后稚补,將異常還原到操作數(shù)棧的棧頂晒杈。拋出異常的字節(jié)碼指令為athrow,該指令的操作碼為0xBF孔厉。
根據(jù)異常表的理解拯钻,編譯器為實(shí)現(xiàn)finally語(yǔ)意,在異常表中多生成了三個(gè)異常項(xiàng)撰豺,捕獲的類(lèi)型為any粪般,即不管任何類(lèi)型的受檢異常,都會(huì)執(zhí)行到target處的字節(jié)碼污桦。
總的理解就是亩歹,當(dāng)try代碼塊中發(fā)生異常時(shí),如果異常類(lèi)型是ArithmeticException凡橱,則跳轉(zhuǎn)到偏移量為19的字節(jié)碼指令小作,如果異常類(lèi)型不是ArithmeticException,則會(huì)匹配到異常表的第二項(xiàng)稼钩,跳動(dòng)到偏移量為32的字節(jié)碼指令顾稀,也就是執(zhí)行finally塊的代碼。異常表的第三項(xiàng)坝撑,如果偏移量為19到22的字節(jié)碼指令在執(zhí)行過(guò)程中拋出異常静秆,不管任何受檢異常都跳轉(zhuǎn)到finally塊執(zhí)行粮揉,偏移量為19到22的字節(jié)碼指令對(duì)應(yīng)catch塊的代碼。
從這個(gè)例子中可以看出抚笔,編譯器除了為try代碼塊或者每個(gè)catch代碼塊都添加一個(gè)異常項(xiàng)用于捕獲任意受檢異常跳轉(zhuǎn)到finally代碼塊執(zhí)行之外扶认,還把finally代碼塊的代碼復(fù)制到try代碼塊的尾部,以及catch代碼塊的尾部殊橙。以此確保任何情況下finally代碼塊中的代碼都會(huì)被執(zhí)行辐宾。
try-with-resource語(yǔ)法糖
在JDK1.7之前,為確保訪問(wèn)的資源被關(guān)閉膨蛮,我們需要為資源的訪問(wèn)代碼塊添加try-finally確保任何情況下資源都能被關(guān)閉叠纹,但由于關(guān)閉資源的close方法也可能拋出異常,因此也需要在finally代碼塊中嵌套try-catch代碼塊鸽疾,這樣寫(xiě)出來(lái)的代碼顯得非常的亂吊洼。
JDK1.7推出了try-with-resource語(yǔ)法糖[1],幫助資源自動(dòng)釋放制肮,不需要在finally塊中顯示的調(diào)用資源的close方法關(guān)閉資源冒窍,由編譯器自動(dòng)生成。try-with-resource語(yǔ)法糖的使用如代碼
public void tryWithResource() {
try (InputStream in = new FileInputStream("/tmp/com.congzhizhi.asmbytecode.book.UseAsmModifyClass.class")) {
} catch (Exception e) {
}
}
從tryWithResource方法編譯后的字節(jié)碼可以看出豺鼻,編譯器為try括號(hào)內(nèi)打開(kāi)的輸入流InputStream综液,在try塊的尾部添加了關(guān)閉輸入流的相關(guān)代碼。自動(dòng)添加的字節(jié)碼指令實(shí)現(xiàn):判斷局部變量in是否為空儒飒,如果不為空則調(diào)用局部變量in的close方法谬莹,并且為調(diào)用close方法的字節(jié)碼指令也添加了try-catch塊。