Java虛擬機的指令由一個字節長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其后的零至多個代表此操作所需的參數(稱為操作數,Operand)構成。由于Java虛擬機采用面向操作數棧而不是面向寄存器的架構,所以大多數指令都不包含操作數,只有一個操作碼,指令參數都存放在操作數棧中。
由于限制了Java虛擬機操作碼的長度為一個字節,致使指令集的操作碼總數不能超過256條。?
1、字節碼與數據類型
大部分與數據類型相關的字節碼指令,他們的操作碼助記符中都有特殊的字符來表明專門為那種數據類型服務:i代表對int類型的數據操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
下表列舉了Java虛擬機所支持的與數據類型相關的字節碼指令,通過使用數據類型列所代表的特殊字符替換opcode列的指令模板中的T,就可以得到一個具體的字節碼指令。如果在表中指令模板與數據類型兩列共同確定的格為空,則說明虛擬機不支持對這種數據類型執行這項操作。
大部分指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器會在編譯期或運行期將byte和short類型的數據帶符號擴展(Sign-Extend)為相應的int類型數據,將boolean和char類型數據零位擴展(Zero-Extend)為相應的int類型數據。與之類似,在處理boolean、byte、short和char類型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理。
2、加載和存儲指令
加載和存儲指令用于將數據在棧幀中的局部變量表和操作數棧之間來回傳輸,這類指令包括:
1)將一個局部變量加載到操作棧:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、 dload_<n>、aload、aload_<n>
2)將一個數值從操作數棧存儲到局部變量表:istore、istore_<n>、lstore、lstore_<n>、fstore、 fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
3)將一個常量加載到操作數棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、 iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
4)擴充局部變量表的訪問索引的指令:wide
存儲數據的操作數棧和局部變量表主要由加載和存儲指令進行操作,除此之外,還有少量指令,如訪問對象的字段或數組元素的指令也會向操作數棧傳輸數據。
3、運算指令
算術指令用于對兩個操作數棧上的值進行某種特定運算,并把結果重新存入到操作棧頂。大體上運算指令可以分為兩種:對整數數據進行運算的指令與對浮點型數據進行運算的指令。整數與浮點數的算術指令在溢出和被零除的時候也有各自不同的行為表現。無論是哪種算術指令,均是使用Java虛擬機的算術類型來進行計算的,換句話說是不存在直接支持byte、short、char和boolean類型的算術指令,對于上述幾種數據的運算,應使用操作int類型的指令代替。所有的算術指令包括:
1)加法指令:iadd、ladd、fadd、dadd
2)減法指令:isub、lsub、fsub、dsub
3)乘法指令:imul、lmul、fmul、dmul
4)除法指令:idiv、ldiv、fdiv、ddiv
5)求余指令:irem、lrem、frem、drem
6)取反指令:ineg、lneg、fneg、dneg
7)位移指令:ishl、ishr、iushr、lshl、lshr、lushr
8)按位或指令:ior、lor
9)按位與指令:iand、land
10)按位異或指令:ixor、lxor
11)局部變量自增指令:iinc
12)比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
4、類型轉換指令
類型轉換指令可以將兩種不同的數值類型相互轉換,這些轉換操作一般用于實現用戶代碼中的顯式類型轉換操作,或者用來處理字節碼指令集中數據類型相關指令無法與數據類型一一對應的問題。
Java虛擬機直接支持(即轉換時無須顯式的轉換指令)以下數值類型的寬化類型轉換(Widening Numeric Conversion,即小范圍類型向大范圍類型的安全轉換):
1)int類型到long、float或者double類型
2)long類型到float、double類型
3)float類型到double類型
與之相對的,處理窄化類型轉換(Narrowing Numeric Conversion)時,就必須顯式地使用轉換指令來完成,這些轉換指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化類型轉換可能會導致轉換結果產生不同的正負號、不同的數量級的情況,轉換過程很可能會導致數值的精度丟失。
5、對象創建與訪問指令
雖然類實例和數組都是對象,但Java虛擬機對類實例和數組的創建與操作使用了不同的字節碼指令。對象創建后,就可以通過對象訪問指令獲取對象實例或者數組實例中的字段或者數組元素,這些指令包括:
1)創建類實例的指令:new
2)創建數組的指令:newarray、anewarray、multianewarray
3)訪問類字段(static字段,或者稱為類變量)和實例字段(非static字段,或者稱為實例變量)的指令:getfield、putfield、getstatic、putstatic
4)把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
5)將一個操作數棧的值儲存到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
6)取數組長度的指令:arraylength
7)檢查類實例類型的指令:instanceof、checkcast
6、操作數棧管理指令
如同操作一個普通數據結構中的堆棧那樣,Java虛擬機提供了一些用于直接操作操作數棧的指令,包括:
1)將操作數棧的棧頂一個或兩個元素出棧:pop、pop2
2)復制棧頂一個或兩個數值并將復制值或雙份的復制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
3)將棧最頂端的兩個數值互換:swap
7、控制轉移指令
控制轉移指令可以讓Java虛擬機有條件或無條件地從指定位置指令(而不是控制轉移指令)的下一條指令繼續執行程序,從概念模型上理解,可以認為控制指令就是在有條件或無條件地修改PC寄存器的值。控制轉移指令包括:
1)條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
2)復合條件分支:tableswitch、lookupswitch
3)無條件分支:goto、goto_w、jsr、jsr_w、ret
在Java虛擬機中有專門的指令集用來處理int和reference類型的條件分支比較操作,為了可以無須明顯標識一個數據的值是否null,也有專門的指令用來檢測null值。
與前面算術運算的規則一致,對于boolean類型、byte類型、char類型和short類型的條件分支比較操作,都使用int類型的比較指令來完成,而對于long類型、float類型和double類型的條件分支比較操作,則會先執行相應類型的比較運算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),運算指令會返回一個整型值到操作數棧中,隨后再執行int類型的條件分支比較操作來完成整個分支跳轉。 由于各種類型的比較最終都會轉化為int類型的比較操作,int類型比較是否方便、完善就顯得尤為重要,而Java虛擬機提供的int類型的條件分支指令是最為豐富、強大的。
8、方法調用和返回指令
方法調用指令包括:
1)invokevirtual指令:用于調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
2)invokeinterface指令:用于調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
3)invokespecial指令:用于調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
4)invokestatic指令:用于調用類靜態方法(static方法)。
5)invokedynamic指令:用于在運行時動態解析出調用點限定符所引用的方法。并執行該方法。前面四條調用指令的分派邏輯都固化在Java虛擬機內部,用戶無法改變,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
方法調用指令與數據類型無關,而方法返回指令是根據返回值的類型區分的,包括ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實例初始化方法、類和接口的類初始化方法使用。
9、異常處理指令
在Java程序中顯式拋出異常的操作(throw語句)都由athrow指令來實現,除了用throw語句顯式拋出異常的情況之外,許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。
而在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的(很久之前曾經使用jsr和ret指令來實現,現在已經不用了),而是采用異常表來完成。
10、同步指令
Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管 程(Monitor,更常見的是直接將它稱為“鎖”)來實現的。
方法級的同步是隱式的,無須通過字節碼指令來控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池中的方法表結構中的ACC_SYNCHRONIZED訪問標志得知一個方法是否被聲明為同步方法。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設置,如果設置了,執行線程就要求先成功持有管程,然后才能執行方法,最后當方法完成(無論是正常完成 還是非正常完成)時釋放管程。在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個同步方法執行期間拋出了異常,并且在方法內部無法處理此異常,那這個同步方法所持有的管程將在異常拋到同步方法邊界之外時自動釋放。
同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的,Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義,正確實現synchronized關鍵字需要Javac編譯器與Java虛擬機兩者共同協作支持。