Java字节码指令简介
本文是《深入理解Java虛擬機(jī)》中第六章的讀書筆記。
1、概述
在Class文件中,Java方法里的方法體,也就是代表著一個(gè)Java源碼程序中程序的部分存儲(chǔ)在方法表集合的Code屬性中。存儲(chǔ)在Code屬性中的是字節(jié)碼,也就是編譯后的程序。Java虛擬機(jī)的指令由兩部分組成,首先是一個(gè)字節(jié)長(zhǎng)度、代表某種含義的數(shù)字(即操作碼),在操作碼后面跟著零個(gè)或多個(gè)代表這個(gè)操作所需的參數(shù)(即操作數(shù))。由于Java虛擬機(jī)采用的是面向操作數(shù)棧而不是寄存器的架構(gòu),所以大多數(shù)指令不包含操作數(shù),只有一個(gè)操作碼。
操作碼的長(zhǎng)度只有一個(gè)字節(jié),這就限制了操作碼的個(gè)數(shù)不超過(guò)256個(gè)。同時(shí),Class文件格式放棄了編譯后代碼的操作數(shù)長(zhǎng)度對(duì)齊,這就意味著虛擬機(jī)處理那些超過(guò)一個(gè)字節(jié)的數(shù)據(jù)時(shí),不得不在運(yùn)行時(shí)從字節(jié)中重建出具體數(shù)據(jù)的結(jié)構(gòu)。比如,如果要將一個(gè)兩個(gè)字節(jié)長(zhǎng)的無(wú)符號(hào)整數(shù)使用兩個(gè)無(wú)符號(hào)字節(jié)存儲(chǔ)起來(lái)分別是byte1和byte2,那么就需要這樣構(gòu)造出原始的無(wú)符號(hào)整數(shù):
(byte1<<8) | byte2
這樣會(huì)在某種程度上導(dǎo)致執(zhí)行字節(jié)碼時(shí)損失一些性能。但這樣做也有好處,那就是由于不需要對(duì)齊,省去了中間的填充與間隔符號(hào);用一個(gè)字節(jié)來(lái)表示操作碼,也是為了獲得短小的編譯代碼。這樣就盡可能的減少了編譯后的代碼的長(zhǎng)度,非常適合網(wǎng)絡(luò)傳輸。
如果不考慮異常處理的話,那么Java虛擬機(jī)的解釋器可以使用下面的偽代碼作為基本的執(zhí)行模型來(lái)理解:
do{自動(dòng)計(jì)算PC寄存器的值加1;根據(jù)PC寄存器的指示位置,從字節(jié)碼流中取出操作碼;if(字節(jié)碼存在操作數(shù)) 從字節(jié)碼流中取出操作數(shù);執(zhí)行操作碼所定義的操作; }while(字節(jié)碼長(zhǎng)度>0);2、字節(jié)碼與數(shù)據(jù)類型
在Java虛擬機(jī)的指令集中,大多數(shù)的指令都包含了操作所對(duì)應(yīng)的數(shù)據(jù)類型信息。比如iload指令表示從局部變量表中加載int型數(shù)據(jù)到操作數(shù)棧中,而fload表示加載float類型的數(shù)據(jù)。不過(guò),這兩條指令再虛擬機(jī)的內(nèi)部可能是由同一段代碼來(lái)實(shí)現(xiàn)的,但在class文件中必須有自己的操作碼。
我們已經(jīng)知道Java指令的長(zhǎng)度只有一個(gè)字節(jié),這就限制了指令集的大小。如果每個(gè)指令都像上面兩個(gè)指令那樣包含所有的數(shù)據(jù)類型,那么就有可能導(dǎo)致指令過(guò)多。因此,Java虛擬機(jī)的指令集對(duì)于特定的操作只提供了有限的類型相關(guān)指令去支持它。比如,大多數(shù)指令沒(méi)有支持整數(shù)類型byte、char和short,甚至沒(méi)有指令支持boolean類型。
這些指令中都有特殊的字符來(lái)表示專門支持的類型:i代表int類型,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表Reference。
這里僅僅介紹一下指令的種類以及作用,并不會(huì)過(guò)多的介紹各個(gè)指令的含義以及使用,需要的話可以查看《Java虛擬機(jī)規(guī)范(Java SE 7版)》。
3、加載和存儲(chǔ)指令
加載指令用于將局部變量表中的數(shù)據(jù)傳送到操作數(shù)棧中,而存儲(chǔ)指令用于將操作數(shù)棧中的結(jié)果傳送到局部變量表中。這類指令包括如下幾種:
- 將一個(gè)局部變量加載到操作棧,比如iload、iload<n>、fload、fload<n>、lload、lload<n>、dload、dload<n>、aload、aload<n>;
- 將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表,比如istore、istore<n>、lstore、lstore<n>、fstore、fstore<n>、dstore、dstore<n>、astore、astore<n>;
- 將一個(gè)常量加載到操作數(shù)棧,比如bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>;
- 擴(kuò)充局部變量表的訪問(wèn)索引的指令:wide;
上面中帶尖括號(hào)的指令實(shí)際是一組指令。比如iload<n>,代表了iload_1、iload_2和iload_3。這幾組指令是某個(gè)帶操作數(shù)的指令(比如iload)的特殊形式,它們省略了操作數(shù),不過(guò)操作數(shù)隱含在指令中。
4、運(yùn)算指令
運(yùn)算或算術(shù)指令用于對(duì)一個(gè)或兩個(gè)操作數(shù)棧上的值進(jìn)行某種特定的運(yùn)算,并將結(jié)果存入棧頂。大體上可以分為兩種,對(duì)整數(shù)進(jìn)行運(yùn)算的指令和對(duì)浮點(diǎn)數(shù)進(jìn)行運(yùn)算的指令。不過(guò),由于沒(méi)有支持byte、short、char和boolean的算術(shù)指令,對(duì)于這些數(shù)據(jù)的運(yùn)算,會(huì)把它們轉(zhuǎn)化為int類型進(jìn)行運(yùn)算。指令列出如下:
- 加法指令:iadd、ladd、fadd、dadd;
- 減法指令:isub、lsub、fsub、dsub;
- 乘法指令:imul、lmul、fmul、dmul;
- 除法指令:idiv、ldiv、fdiv、ddiv;
- 求余指令:irem、lrem、frem、drem;
- 取反指令:ineg、lneg、fneg、dneg;
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr;
- 按位或指令:ior、lor;
- 按位與指令:iand、land;
- 按位異或指令:ixor、lxor;
- 局部變量自增指令:iinc;
- 比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp;
5、類型轉(zhuǎn)換指令
類型轉(zhuǎn)換指令用來(lái)將兩種不同類型進(jìn)行轉(zhuǎn)換,這些轉(zhuǎn)換操作一般用于實(shí)現(xiàn)代碼中的顯示類型轉(zhuǎn)換操作,或者前面提到的字節(jié)碼指令集中數(shù)據(jù)類型相關(guān)指令無(wú)法與數(shù)據(jù)類型一一對(duì)應(yīng)的問(wèn)題。
虛擬機(jī)直接支持寬化類型轉(zhuǎn)換,即小范圍類型向大范圍類型的安全轉(zhuǎn)換,不需要顯示的轉(zhuǎn)換指令。
但是處理窄化類型轉(zhuǎn)換時(shí),必須顯示使用轉(zhuǎn)換指令來(lái)完成,這些指令包括:i2b、i2c、i2s、l2i、f2l、d2i、d2l和d2f。這些指令可能會(huì)導(dǎo)致數(shù)值的精度丟失。
6、對(duì)象創(chuàng)建與訪問(wèn)指令
雖然類實(shí)例和數(shù)組都是對(duì)象,但是虛擬機(jī)創(chuàng)建類對(duì)象和數(shù)組的指令是不同的。對(duì)象創(chuàng)建后,就可以通過(guò)對(duì)象訪問(wèn)指令獲取對(duì)象實(shí)例或者數(shù)組實(shí)例中的字段或數(shù)組元素,指令如下:
- 創(chuàng)建類實(shí)例的指令:new;
- 創(chuàng)建數(shù)組的指令:newarray、anewarray、multianewarray;
- 訪問(wèn)類字段和實(shí)例字段的指令:getfield、putfield、getstatic、putstatic;
- 把一個(gè)數(shù)組元素加載到操作數(shù)棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload;
- 將一個(gè)操作數(shù)棧的值存儲(chǔ)到數(shù)組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore;
- 取數(shù)組長(zhǎng)度的指令:arraylength;
- 檢查類實(shí)例類型的指令:instanceof、checkcast;
7、操作數(shù)棧管理指令
就像操作一個(gè)普通的棧一樣,Java虛擬機(jī)提供了一些用于直接操作操作數(shù)棧的指令,包括:
- 將操作數(shù)棧的棧頂一個(gè)或兩個(gè)元素出棧:pop、pop2;
- 復(fù)制棧頂一個(gè)或兩個(gè)數(shù)值并將復(fù)制值或雙份的復(fù)制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2;
- 將棧頂最頂端的兩個(gè)數(shù)值互換:swap;
8、控制轉(zhuǎn)移指令
控制轉(zhuǎn)移指令可以讓Java虛擬機(jī)有條件或無(wú)條件的從指定的位置指令而不是控制轉(zhuǎn)移指令的下一條指令繼續(xù)執(zhí)行,可以理解為控制轉(zhuǎn)移指令改變了PC寄存器的值。指令如下:
- 條件分支:ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne;
- 復(fù)合條件分支:tableswitch、lookupswitch;
- 無(wú)條件分支:goto、goto_w、jsr、jsr_w、ret;
9、方法調(diào)用和返回指令
這里僅僅列出5條用于方法調(diào)用的指令:
- invokevirtual指令用于調(diào)用對(duì)象的實(shí)例方法,根據(jù)對(duì)象的實(shí)際類型進(jìn)行分派(虛方法分派),這也是Java語(yǔ)言中最常見的方法分派方式;
- invokeinterface指令用于調(diào)用接口方法,它會(huì)在運(yùn)行時(shí)搜索一個(gè)實(shí)現(xiàn)了這個(gè)接口方法的對(duì)象,找出適合的方法進(jìn)行調(diào)用;
- invokespecial指令用于調(diào)用一些需要特殊處理的實(shí)例方法,包括實(shí)例初始化方法、私有方法和父類方法;
- invokestatic指令用于調(diào)用類方法(static方法);
- invokedynamic指令用于在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符索引用的方法,并執(zhí)行方法,前面4條指令的分派邏輯都固化在Java虛擬機(jī)內(nèi)部,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的;
方法調(diào)用指令與類型無(wú)關(guān),但是方法返回指令是根據(jù)返回值的類型區(qū)分的,包括ireturn、lreturn、freturn、dreturn和areturn,另外還有一個(gè)return指令供聲明為void的方法、實(shí)例初始化方法以及類和接口的類初始化方法使用。
10、異常處理指令
在Java程序中顯式拋出異常的操作(throw語(yǔ)句)都是由athrow指令來(lái)實(shí)現(xiàn)的,除了用throw語(yǔ)句顯式拋出異常外,Java虛擬機(jī)規(guī)范還規(guī)定了許多運(yùn)行時(shí)異常會(huì)在其他Java虛擬機(jī)指令檢測(cè)到異常狀況時(shí)自動(dòng)拋出。
而在Java虛擬機(jī)中,處理異常(catch語(yǔ)句)不是由字節(jié)碼指令來(lái)完成的,而是采用異常表來(lái)完成的。
11、同步指令
Java虛擬機(jī)可以支持方法級(jí)的同步和方法內(nèi)部一段指令序列的同步,這兩種同步結(jié)構(gòu)都是使用管程(Monitor)來(lái)支持的。
方法級(jí)的同步是隱式的,即不需要通過(guò)字節(jié)碼指令來(lái)控制,它實(shí)現(xiàn)在方法調(diào)用和返回操作中。虛擬機(jī)可以從方法常量池的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZED訪問(wèn)標(biāo)志得知一個(gè)方法是否聲明為同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令就會(huì)去檢查方法的ACC_SYNCHRONIZED訪問(wèn)標(biāo)志是否被設(shè)置了,如果設(shè)置,執(zhí)行線程就要求持有管程。在方法執(zhí)行期間,執(zhí)行線程持有了管程,其他任何線程都無(wú)法再獲取到同一個(gè)管程。如果一個(gè)方法在執(zhí)行期間發(fā)生了異常,并在方法中無(wú)法處理次異常,那么這個(gè)同步方法所持有的管程將在異常拋出后自動(dòng)釋放。
同步一段指令集序列通常是由Java語(yǔ)言中的synchronized語(yǔ)句塊表示的,Java虛擬機(jī)的指令集中有monitorenter和monitorexit指令來(lái)支持synchronized關(guān)鍵字的語(yǔ)義。正確實(shí)現(xiàn)synchronized關(guān)鍵字需要Javac編譯器和Java虛擬機(jī)兩者共同協(xié)作。編譯器必須保證每個(gè)monitorenter指令都有對(duì)應(yīng)的monitorexit指令。
添加公眾號(hào)Machairodus,我會(huì)不時(shí)分享一些平時(shí)學(xué)到的東西~
總結(jié)
以上是生活随笔為你收集整理的Java字节码指令简介的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: LOL中APEZ是出法穿鞋还是CD鞋?
- 下一篇: Java NIO Channel