位元組碼指令簡介 Java虛擬機的指令由一個位元組長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需參數(稱為操作數,Operands)而構成。由於Java虛擬機採用面向操作數棧而不是寄存器的架構(這兩種架構的區別和影響將在第8章中探討),所以大多數的指 ...
Java虛擬機的指令由一個位元組長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需參數(稱為操作數,Operands)而構成。由於Java虛擬機採用面向操作數棧而不是寄存器的架構(這兩種架構的區別和影響將在第8章中探討),所以大多數的指令都不包含操作數,只有一個操作碼。
位元組碼指令集是一種具有鮮明特點、優劣勢都很突出的指令集架構,由於限制了Java虛擬機操作碼的長度為一個位元組(即0~255),這意味著指令集的操作碼總數不可能超過256條;又由於Class文件格式放棄了編譯後代碼的操作數長度對齊,這就意味著虛擬機處理那些超過一個位元組數據的時候,不得不在運行時從位元組中重建出具體數據的結構.
如果要將一個16位長度的無符號整數使用兩個無符號位元組存儲起來(將它們命名為byte1和byte2),那它們的值應該是這樣的:
(byte1 << 8) | byte2
這種操作在某種程度上會導致解釋執行位元組碼時損失一些性能。但這樣做的優勢也非常明顯,放棄了操作數長度對齊[插圖],就意味著可以省略很多填充和間隔符號;用一個位元組來代表操作碼,也是為了儘可能獲得短小精幹的編譯代碼。這種追求儘可能小數據量、高傳輸效率的設計是由Java語言設計之初面向網路、智能家電的技術背景所決定的,並一直沿用至今。
1 、位元組碼與數據類型
在Java虛擬機的指令集中,大多數的指令都包含了其操作所對應的數據類型信息。對於大部分與數據類型相關的位元組碼指令,它們的操作碼助記符中都有特殊的字元來表明專門為哪種數據類型服務:i代表對int類型的數據操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助記符中沒有明確地指明操作類型的字母,如arraylength指令,它沒有代表數據類型的特殊字元,但操作數永遠只能是一個數組類型的對象。還有另外一些指令,如無條件跳轉指令goto則是與數據類型無關的。
2、載入和存儲指令
載入和存儲指令用於將數據在棧幀中的局部變數表和操作數棧(見第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。
(5)取反指令:ineg、lneg、fneg、dneg。
(6)位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
(7)按位或指令:ior、lor。
(8)按位與指令:iand、land。
(10)按位異或指令:ixor、lxor。
(11)局部變數自增指令:iinc。
(12)比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
4、類型轉換指令
可以將兩種不同的數值類型進行相互轉換,這些轉換操作一般用於實現用戶代碼中的顯式類型轉換操作,或者用來處理本節開篇所提到的位元組碼指令集中數據類型相關指令無法與數據類型一一對應的問題。Java虛擬機直接支持(即轉換時無需顯式的轉換指令)以下數值類型的寬化類型轉換(Widening Numeric Conversions,即小範圍類型向大範圍類型的安全轉換):
(1)int類型到long、float或者double類型。
(2)long類型到float、double類型。
(3)float類型到double類型。
在將一個浮點值窄化轉換為整數類型T(T限於int或long類型之一)的時候,將遵循以下轉換規則:
(1)如果浮點值是NaN,那轉換結果就是int或long類型的0。
(2)如果浮點值不是無窮大的話,浮點值使用IEEE 754的向零舍入模式取整,獲得整數值v,如果v在目標類型T(int或long)的表示範圍之內,那轉換結果就是v。
(3)否則,將根據v的符號,轉換為T所能表示的最大或者最小正數。
5、對象創建與訪問指令
雖然類實例和數組都是對象,但Java虛擬機對類實例和數組的創建與操作使用了不同的位元組碼指令(在第7章會講到數組和普通類的類型創建過程是不同的)。對象創建後,就可以通過對象訪問指令獲取對象實例或者數組實例中的欄位或者數組元素,這些指令如下。
(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,見6.4.3節),運算指令會返回一個整型值到操作數棧中,隨後再執行int類型的條件分支比較操作來完成整個分支跳轉。由於各種類型的比較最終都會轉化為int類型的比較操作,int類型比較是否方便完善就顯得尤為重要,所以Java虛擬機提供的int類型的條件分支指令是最為豐富和強大的。
8 、方法調用和返回指令
方法調用(分派、執行過程)將在第8章具體講解,這裡僅列舉以下5條用於方法調用的指令。
(1)invokevirtual指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
(2)invokeinterface指令用於調用介面方法,它會在運行時搜索一個實現了這個介面方法的對象,找出適合的方法進行調用。
(3)invokespecial指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
(4)invokestatic指令用於調用類方法(static方法)。
(5)invokedynamic指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面4條調用指令的分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
方法調用指令與數據類型無關,而方法返回指令是根據返回值的類型區分的,包括ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實例初始化方法以及類和介面的類初始化方法使用。
9 、異常處理指令
在Java程式中顯式拋出異常的操作(throw語句)都由athrow指令來實現,除了用throw語句顯式拋出異常情況之外,Java虛擬機規範還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。例如,在前面介紹的整數運算中,當除數為零時,虛擬機會在idiv或ldiv指令中拋出ArithmeticException異常。而在Java虛擬機中,處理異常(catch語句)不是由位元組碼指令來實現的(很久之前曾經使用jsr和ret指令來實現,現在已經不用了),而是採用異常表來完成的。
10、同步指令
Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支持的。
方法級的同步是隱式的,即無須通過位元組碼指令來控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標誌得知一個方法是否聲明為同步方法。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設置,如果設置了,執行線程就要求先成功持有管程,然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那麼這個同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。
同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的,Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義,正確實現synchronized關鍵字需要Javac編譯器與Java虛擬機兩者共同協作支持,譬如如下代碼所示的代碼。
void onlyMe(Foo f){
synchronized(f){
doSomeThing();
}
}
編譯後,這段代碼生成的位元組碼序列如下:
編譯器必須確保無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須執行其對應的monitorexit指令,而無論這個方法是正常結束還是異常結束。從上面的示例代碼的位元組碼序列中可以看到,為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行monitorexit指令。