- Java虛擬機的指令由一個字節(jié)長度的、代表著某種特定操作含義的數(shù)字(稱為操作碼,Opcode)以及跟隨其后的零至多個代表此操作所需參數(shù)(稱為操作數(shù),Operands)而構(gòu)成。由于Java虛擬機采用面向操作數(shù)棧而不是寄存器的架構(gòu),所以大多數(shù)的指令都不包含操作數(shù),只有一個操作碼。
- 由于限制了Java虛擬機操作碼的長度為一個字節(jié)(即0~255),這意味著指令集的操作碼總數(shù)不可能超過256條;又由于Class文件格式放棄了編譯后代碼的操作數(shù)長度對齊,這就意味著虛擬機處理那些超過一個字節(jié)數(shù)據(jù)的時候,不得不在運行時從字節(jié)中重建出具體數(shù)據(jù)的結(jié)構(gòu),如果要將一個16位長度的無符號整數(shù)使用兩個無符號字節(jié)存儲起來(將它們命名為byte1和byte2),那它們的值應(yīng)該是這樣的:
(byte1<<8)|byte2
放棄了操作數(shù)長度對齊,可以省略很多填充和間隔符號;用一個字節(jié)來代表操作碼,也是為了盡可能獲得短小精干的編譯代碼。
do{ 自動計算PC寄存器的值加1;
根據(jù)PC寄存器的指示位置,從字節(jié)碼流中取出操作碼;
if(字節(jié)碼存在操作數(shù))從字節(jié)碼流中取出操作數(shù);
執(zhí)行操作碼所定義的操作;
}while(字節(jié)碼流長度>0);
一、字節(jié)碼與數(shù)據(jù)類型
在Java虛擬機的指令集中,大多數(shù)的指令都包含了其操作所對應(yīng)的數(shù)據(jù)類型信息.
對于大部分與數(shù)據(jù)類型相關(guān)的字節(jié)碼指令,它們的操作碼助記符中都有特殊的字符來表明專門為哪種數(shù)據(jù)類型服務(wù):i代表對int類型的數(shù)據(jù)操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助記符中沒有明確地指明操作類型的字母,如arraylength指令,它沒有代表數(shù)據(jù)類型的特殊字符,但操作數(shù)永遠只能是一個數(shù)組類型的對象。還有另外一些指令,如無條件跳轉(zhuǎn)指令goto則是與數(shù)據(jù)類型無關(guān)的。
由于Java虛擬機的操作碼長度只有一個字節(jié),Java虛擬機的指令集對于特定的操作只提供了有限的類型相關(guān)指令去支持它,指令集將會故意被設(shè)計成非完全獨立的(Java虛擬機規(guī)范中把這種特性稱為“Not Orthogonal”,即并非每種數(shù)據(jù)類型和每一種操作都有對應(yīng)的指令)。有一些單獨的指令可以在必要的時候用來將一些不支持的類型轉(zhuǎn)換為可被支持的類型。
表6-31列舉了Java虛擬機所支持的與數(shù)據(jù)類型相關(guān)的字節(jié)碼指令,通過使用數(shù)據(jù)類型列所代表的特殊字符替換opcode列的指令模板中的T,就可以得到一個具體的字節(jié)碼指令。如果在表中指令模板與數(shù)據(jù)類型兩列共同確定的格為空,則說明虛擬機不支持對這種數(shù)據(jù)類型執(zhí)行這項操作。例如,load指令有操作int類型的iload,但是沒有操作byte類型的同類指令。

大部分的指令都沒有支持整數(shù)類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器會在編譯期或運行期將byte和short類型的數(shù)據(jù)帶符號擴展(Sign-Extend)為相應(yīng)的int類型數(shù)據(jù),將boolean和char類型數(shù)據(jù)零位擴展(Zero-Extend)為相應(yīng)的int類型數(shù)據(jù)。與之類似,在處理boolean、byte、short和char類型的數(shù)組時,也會轉(zhuǎn)換為使用對應(yīng)的int類型的字節(jié)碼指令來處理。因此,大多數(shù)對于boolean、byte、short和char類型數(shù)據(jù)的操作,實際上都是使用相應(yīng)的int類型作為運算類型(Computational Type)。
二、指令簡單分類
1.加載和存儲指令
用于將數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來回傳輸,這類指令包括如下內(nèi)容。
將一個局部變量加載到操作棧:
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
將一個數(shù)值從操作數(shù)棧存儲到局部變量表:
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
將一個常量加載到操作數(shù)棧:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
擴充局部變量表的訪問索引的指令:
wide
存儲數(shù)據(jù)的操作數(shù)棧和局部變量表主要就是由加載和存儲指令進行操作,除此之外,還有少量指令,如訪問對象的字段或數(shù)組元素的指令也會向操作數(shù)棧傳輸數(shù)據(jù)。
上面所列舉的指令助記符中,有一部分是以尖括號結(jié)尾的(例如iload_<n>),這些指令助記符實際上是代表了一組指令(例如iload_<n>,它代表了iload_0、iload_1、iload_2和iload_3這幾條指令)。這幾組指令都是某個帶有一個操作數(shù)的通用指令(例如iload)的特殊形式,對于這若干組特殊指令來說,它們省略掉了顯式的操作數(shù),不需要進行取操作數(shù)的動作,實際上操作數(shù)就隱含在指令中。除了這點之外,它們的語義與原生的通用指令完全一致(例如iload_0的語義與操作數(shù)為0時的iload指令語義完全一致)。
2.運算指令
用于對兩個操作數(shù)棧上的值進行某種特定運算,并把結(jié)果重新存入到操作棧頂。
可以分為兩種:整型+浮點型 數(shù)據(jù)進行運算的指令,無論是哪種算術(shù)指令,都使用Java虛擬機的數(shù)據(jù)類型,由于沒有直接支持byte、short、char和boolean類型的算術(shù)指令,對于這類數(shù)據(jù)的運算,應(yīng)使用操作int類型的指令代替。整數(shù)與浮點數(shù)的算術(shù)指令在溢出和被零除的時候也有各自不同的行為表現(xiàn),所有的算術(shù)指令如下。
加法指令: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
3.類型轉(zhuǎn)換指令
可以將兩種不同的數(shù)值類型進行相互轉(zhuǎn)換
Java虛擬機直接支持(即轉(zhuǎn)換時無需顯式的轉(zhuǎn)換指令)以下數(shù)值類型的寬化類型轉(zhuǎn)換(Widening Numeric Conversions,即小范圍類型向大范圍類型的安全轉(zhuǎn)換):
int類型到long、float或者double類型
long類型到float、double類型
float類型到double類型
處理窄化類型轉(zhuǎn)換(Narrowing Numeric Conversions)時,必須顯式地使用轉(zhuǎn)換指令來完成,這些轉(zhuǎn)換指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化類型轉(zhuǎn)換可能會導致轉(zhuǎn)換結(jié)果產(chǎn)生不同的正負號、不同的數(shù)量級的情況,轉(zhuǎn)換過程很可能會導致數(shù)值的精度丟失。
在將int或long類型窄化轉(zhuǎn)換為整數(shù)類型T的時候,轉(zhuǎn)換過程僅僅是簡單地丟棄除最低位N個字節(jié)以外的內(nèi)容,N是類型T的數(shù)據(jù)類型長度,這將可能導致轉(zhuǎn)換結(jié)果與輸入值有不同的正負號。符號位處于數(shù)值的最高位,高位被丟棄之后,轉(zhuǎn)換結(jié)果的符號就取決于低N個字節(jié)的首位了。
在將一個浮點值窄化轉(zhuǎn)換為整數(shù)類型T(T限于int或long類型之一)的時候,將遵循以下轉(zhuǎn)換規(guī)則:
如果浮點值是NaN,那轉(zhuǎn)換結(jié)果就是int或long類型的0。
如果浮點值不是無窮大的話,浮點值使用IEEE 754的向零舍入模式取整,獲得整數(shù)值v,如果v在目標類型T(int或long)的表示范圍之內(nèi),那轉(zhuǎn)換結(jié)果就是v。否則,將根據(jù)v的符號,轉(zhuǎn)換為T所能表示的最大或者最小正數(shù)。
從double類型到float類型的窄化轉(zhuǎn)換過程與IEEE 754中定義的一致,通過IEEE 754向最接近數(shù)舍入模式舍入得到一個可以使用float類型表示的數(shù)字。如果轉(zhuǎn)換結(jié)果的絕對值太小而無法使用float來表示的話,將返回float類型的正負零。如果轉(zhuǎn)換結(jié)果的絕對值太大而無法使用float來表示的話,將返回float類型的正負無窮大,對于double類型的NaN值將按規(guī)定轉(zhuǎn)換為float類型的NaN值。
4.對象創(chuàng)建與訪問指令
雖然類實例和數(shù)組都是對象,但Java虛擬機對類實例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令。對象創(chuàng)建后,就可以通過對象訪問指令獲取對象實例或者數(shù)組實例中的字段或者數(shù)組元素,這些指令如下。
創(chuàng)建類實例的指令:new
創(chuàng)建數(shù)組的指令:newarray、anewarray、multianewarray
訪問類字段和實例字段的指令:getfield、putfield、getstatic、putstatic
把一個數(shù)組元素加載到操作數(shù)棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
將一個操作數(shù)棧的值存儲到數(shù)組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
取數(shù)組長度的指令:arraylength
檢查類實例類型的指令:instanceof、checkcast
5.操作數(shù)棧管理指令
將操作數(shù)棧的棧頂一個或兩個元素出棧:pop、pop2
復制棧頂一個或兩個數(shù)值并將復制值或雙份的復制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
將棧最頂端的兩個數(shù)值互換:swap
6.控制轉(zhuǎn)移指令
可以讓Java虛擬機有條件或無條件地從指定的位置指令 下一條指令繼續(xù)執(zhí)行程序,在有條件或無條件地修改PC寄存器的值。
條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
復合條件分支:tableswitch、lookupswitch
無條件分支:goto、goto_w、jsr、jsr_w、ret
在Java虛擬機中有專門的指令集用來處理int和reference類型的條件分支比較操作,為了可以無須明顯標識一個實體值是否null,也有專門的指令用來檢測null值。
對于boolean、byte、char和short的條件分支比較操作,都是使用int類型的比較指令來完成,而對于long、float和double的條件分支比較操作,則會先執(zhí)行相應(yīng)類型的比較運算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp,見6.4.3節(jié)),運算指令會返回一個整型值到操作數(shù)棧中,隨后再執(zhí)行int類型的條件分支比較操作來完成整個分支跳轉(zhuǎn)。由于各種類型的比較最終都會轉(zhuǎn)化為int類型的比較操作,int類型比較是否方便完善就顯得尤為重要,所以Java虛擬機提供的int類型的條件分支指令是最為豐富和強大的。
7.方法調(diào)用和返回指令
invokevirtual 調(diào)用對象的實例方法,根據(jù)對象的實際類型進行分派(虛方法分派)
invokeinterface 調(diào)用接口方法,它會在運行時搜索一個實現(xiàn)了這個接口方法的對象,找出適合的方法進行調(diào)用invokespecial 調(diào)用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法
invokestatic調(diào)用類方法
invokedynamic在運行時動態(tài)解析出調(diào)用點限定符所引用的方法,并執(zhí)行該方法,前面4條調(diào)用指令的分派邏輯都固化在Java虛擬機內(nèi)部,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導方法決定的。
方法調(diào)用指令與數(shù)據(jù)類型無關(guān),而方法返回指令是根據(jù)返回值的類型區(qū)分的,包括ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實例初始化方法以及類和接口的類初始化方法使用。
8.異常處理指令
在Java程序中顯式拋出異常的操作(throw語句)都由athrow指令來實現(xiàn),在其他Java虛擬機指令檢測到異常狀況時也自動拋出。
而在Java虛擬機中,處理異常(catch語句)不是由字節(jié)碼指令來實現(xiàn)的,而是采用異常表來完成的。
9.同步指令
Java虛擬機可以支持方法級的同步和方法內(nèi)部一段指令序列的同步,這兩種同步結(jié)構(gòu)都是使用管程(Monitor)來支持的。
方法級的同步是隱式的,即無須通過字節(jié)碼指令來控制,它實現(xiàn)在方法調(diào)用和返回操作之中。虛擬機可以從方法常量池的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZED訪問標志得知一個方法是否聲明為同步方法。當方法調(diào)用時,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程就要求先成功持有管程,然后才能執(zhí)行方法,最后當方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執(zhí)行期間,執(zhí)行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個同步方法執(zhí)行期間拋出了異常,并且在方法內(nèi)部無法處理此異常,那么這個同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。
同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的,Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關(guān)鍵字的語義,正確實現(xiàn)synchronized關(guān)鍵字需要Javac編譯器與Java虛擬機兩者共同協(xié)作支持,
代碼:
void onlyMe(Foo f){
synchronized(f){
System.out.println();
}
}
javap反編譯:
void onlyMe(org.clazz.Foo);
flags:
Code:
stack=2, locals=4, args_size=2
0: aload_1 //將對象f入棧
1: dup //復制棧頂元素(即f的引用)
2: astore_2 //將棧頂元素存儲到局部變量表slot 2中
3: monitorenter //以棧頂元素(即f)作為鎖,開始同步
4: getstatic #3 // Field java/lang/System.out:Ljav
a/io/PrintStream;
7: invokevirtual #4 // Method java/io/PrintStream.prin
tln:()V
10: aload_2 //將局部變量Slow 2的元素(即f)入棧
11: monitorexit //退出同步
12: goto 20 // 方法正常結(jié)束,跳轉(zhuǎn)到18返回
15: astore_3 //從這步開始是異常路徑,見下面異常表的target 15
16: aload_2 //將局部變量Slow 2的元素(即f)入棧
17: monitorexit //退出同步
18: aload_3 //將局部變量Slow 3的元素(即異常對象)入棧
19: athrow //把異常對象重新拋出給onlyMe()方法的調(diào)用者
20: return //方法正常返回
Exception table:
from to target type
4 12 15 any
15 18 15 any
LineNumberTable:
line 18: 0
line 19: 4
line 20: 10
line 21: 20
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 15
locals = [ class org/clazz/TestClass, class org/clazz/Foo, class java/
lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
編譯器必須確保無論方法通過何種方式完成,方法中調(diào)用過的每條monitorenter指令都必須執(zhí)行其對應(yīng)的monitorexit指令,而無論這個方法是正常結(jié)束還是異常結(jié)束。
從上述字節(jié)碼序列中可以看到,為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正確配對執(zhí)行,編譯器會自動產(chǎn)生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行monitorexit指令。
三、公有設(shè)計和私有實現(xiàn)
Java虛擬機規(guī)范描繪了Java虛擬機應(yīng)有的共同程序存儲格式:Class文件格式以及字節(jié)碼指令集。
Java虛擬機實現(xiàn)必須能夠讀取Class文件并精確實現(xiàn)包含在其中的Java虛擬機代碼的語義。拿著Java虛擬機規(guī)范一成不變地逐字實現(xiàn)其中要求的內(nèi)容當然是一種可行的途徑,但一個優(yōu)秀的虛擬機實現(xiàn),在滿足虛擬機規(guī)范的約束下對具體實現(xiàn)做出修改和優(yōu)化也是完全可行的,并且虛擬機規(guī)范中明確鼓勵實現(xiàn)者這樣做。只要優(yōu)化后Class文件依然可以被正確讀取,并且包含在其中的語義能得到完整的保持,那實現(xiàn)者就可以選擇任何方式去實現(xiàn)這些語義,虛擬機后臺如何處理Class文件完全是實現(xiàn)者自己的事情,只要它在外部接口上看起來與規(guī)范描述的一致即可。
將輸入的Java虛擬機代碼在加載或執(zhí)行時翻譯成另外一種虛擬機的指令集。
將輸入的Java虛擬機代碼在加載或執(zhí)行時翻譯成宿主機CPU的本地指令集(即JIT代碼生成技術(shù))。