字节码指令
虛擬機是一個相對于物理機的概念,這兩種機器都有代碼執行能力,其區別在于物理機的執行引擎是直接建立在 CPU 處理器、指令集、操作系統和硬件層面上的。
而虛擬機的執行引擎則由自己實現,因此可以制定自己的指令集和執行引擎的結構體系,而且還可以執行一些不被硬件直接支持的指令集格式。這就是虛擬機相對于物理機的優勢所在。
但是缺點也比較明顯,由于多了一層虛擬指令,執行虛擬機指令后還要轉化為本地機器碼,所以在執行效率上,虛擬機是不如物理機的。
Java 虛擬機的字節碼指令由一個字節長度的操作碼(Opcode)以及緊隨其后的零至多個操作數(Operands)構成。
如果忽略異常處理,那么 Java 虛擬機的解釋器通過下面這個偽代碼的循環即可有效工作:
do{自動計算pc寄存器以及從pc寄存器的位置取出操作碼;if(存在操作數){取出操作數;}執行操作碼所定義的操作; } while(處理下一次循環);由于字節碼指令集限制了其操作碼長度為 1 個字節(0 ~ 255),即意味著整個指令集中包含的指令總數不超過 256 條。
在虛擬機處理超過 1 個字節的數據時,會在運行時重新構建出具體的數據結構。
例如:如果要將一個 16 位無符號的整數使用兩個無符號字節存儲起來(命名為 byte1 和 byte2),那么這個 16 位無符號數的值應該這樣表示:
(byte1 << 8) | byte2這種操作在某種程度上會導致執行字節碼時損失一些性能。但這樣做的優勢也非常明顯,放棄了操作數長度對齊,就意味著可以節省很多填充和間隔符號;用一個字節來代表操作碼,也是為了盡可能獲得短小精干的編譯代碼。
這種追求盡可能小數據量、高傳輸效率 的設計是由 Java 語言設計之初面向網絡、智能家電的技術背景所決定的并沿用至今。
字節碼與數據類型
在講字節碼指令之前,我們需要了解下,字節碼指令操作的操作數是什么類型的,這些 Java 虛擬機中的數值類型又和 Java 編程語言中的 8 大基本數據類型如何對應的?
Java 語言中的 8 大基本數據類型:
- 整型:byte、short、int、long
- 浮點型:double、float
- 字符型:char
- 布爾型:boolean
Java 程序語言中定義了 8 大基本數據類型,但是在 Java 虛擬機中只分為兩大類:
- 原始類型(primitive type)
- 引用類型(reference type)
原始類型對應的數值稱為原始值、引用類型的數值稱為引用值。
原始類型
原始類型包括如下類型。
- 數值類型
數值類型包括:byte、short、int、long、char、float、double。
- boolean 類型
boolean 類型的值有兩種:true 和 false,默認為 false,雖然在 Java 虛擬機中定義了 boolean 這種類型,但是卻沒有指令直接支持其操作。
所以,對 boolean 類型都需要在編譯后用虛擬機中的 int 類型來表示 —— 1 表示 true、0 表示 false。
- returnAddress 類型
returnAddress 類型表示一個指向某個操作碼 opcode 的指針,此操作碼與虛擬機指令相對應。
引用類型
引用類型包括如下類型。
- 類類型(class type)
- 數組類型(array type)
- 接口類型(interface type)
這三種引用類型的值分別指向動態創建的類實例、數組實例和實現了某個接口的類/數組實例。
在引用類型中還有一個特殊的值 null,當一個引用不指向任何對象時,它就用 null 表示, null 作為引用類型的初始默認值可以轉型成任意的引用類型。
加載與存儲指令
加載和存儲指令用于將數據在棧幀中的局部變量表和操作數棧之間來回傳輸,這類指令包括如下內容。
- 將一個局部變量加載到操作數棧
iload、iload_<n>、lload,load<n>、fload,fload_<n>、dload,dload_<n>、aload,aload_<n>
- 將一個數值從操作數棧存儲到局部變量表
istore、istore_<n>、lstore、lstore_<n>、fstore,fstore_<n>、dstore、dstore_<n>、astore,astore_<n>
- 將一個常量加載到操作數棧
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
- 擴充局部變量表的訪問索引:wide
上面所列舉的指令助記符中,有一部分是以 _<n> 尾的,這些指令助記符實際上是代表了一組指令。
如:iload_<n> 代表了 iload_0、iload_1、iload_2 和 iload_3這幾條指令,此時操作數隱藏于指令之中。
iload_0 表示從當前棧幀局部變量表中 0 號位置取 int 類型的數值加載到操作數棧 iload_1 表示從當前棧幀局部變量表中 1 號位置取 int 類型的數值加載到操作數棧 ...運算指令
算術指令用于對兩個操作數棧上的值進行某種特定運算,并把結構重新壓入操作數棧。
大體上算術指令可以分為兩種:對整型數據進行運算的指令和對浮點類型數據進行運算的指令。
在每一大類中,都有針對 Java 虛擬機具體數據類型的專用算術指令。但沒有直接支持 byte、short、char 和 boolean 類型的算術指令,對于這些數據的運算,都是用 int 類型指令來處理。
所有算術指令包括:
- 加法指令: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
類型轉換指令
類型轉換指令可以在兩種 Java 虛擬機數值類型之間相互轉換。這些轉換操作一般用于實現用戶代碼中的顯式類型轉換操作,或者用來解決 Java 虛擬機字節碼指令的不完備問題。
Java 虛擬機直接支持以下數值的寬化類型轉換(widening numeric conversion,小范圍類型向大范圍類型的安全轉換):
- 從 int 類型到 long、float 或者 double 類型
- 從 long 類型到 float、double 類型
- 從 float 類型到 double 類型
寬化類型轉換指令包括:i2l、i2f、i2d、l2f、l2d 和 f2d。
Java 虛擬機也支持以下窄化類型轉換:
- 從 int 類型到 byte、short 或者 char 類型
- 從 long 類型到 int 類型
- 從 float 類型到 int 或者 long 類型
- 從 double 類型到 int、long 或者 float 類型
窄化類型轉換(narrowing numeric conversion)指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。
對象創建與訪問指令
在 Java 中類實例和數組都是對象,但是 Java 虛擬機對類 class 對象和數組對象的創建使用了不同的字節碼指令。
- 創建類實例的指令:new
- 創建數組的指令:newarray、anewarray、multianewarray
- 訪問類變量(static 字段)的指令:getstatic、putstatic
- 訪問實例變量的指令:getfield、putfield
- 將一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 將一個操作數棧的值存到數組元素中的指令:bastore、castore、sastore、iastor、fastore、dastore、aastore
- 取數組長度的指令:arraylength
- 檢查類實例類型的指令:instanceof、checkcast
操作數棧管理指令
Java 虛擬機提供了一些用于直接操控操作數棧的指令,包括:pop、pop2、dup、dup2、dup_x1、dup_x2、dup2_x1、dup_x2、dup2_x2 和 swap。
控制轉移指令
控制轉移指令可以讓 Java 虛擬機有條件或無條件地從指定指令而不是控制轉移指令的下一條指令繼續執行程序。
控制指令包括:
- 條件分支
ifeq、iflt、ifle、ifne、ifgt、ifge、jfnull、ifnonnull、ificmpeq、ificmpne、ificmplt、ificmpgt、if_icmple、if_icmpge、if_acmpeq、if_acmpne
- 符合條件分支
tableswitch、lookupswitch
- 無條件分支
goto、goto_w、jsr、jsr_w、ret
方法調用與返回指令
以下 5 條指令用于方法調用:
- invokevirtual
用于調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是 Java 語言中最常見的方法分派方式。
- invokeinterface
用于調用接口方法,它會在運行時搜索一個實現了此接口的對象,找出合適的方法進行調用。
- invokestatic
用于調用類方法(static 方法)。
- invokedynamic
指令用于在運行時動態解析出調用點限定符所引用的方法,并執行該方法,前面的 4 條調用指令的分派邏輯都固話在 Java 虛擬機內部,而 invokedynamic 指令的分派邏輯則是由用戶所設定的引導方法所決定的。
異常處理指令
在 Java 程序中顯示拋出異常的操作(throw 語句)都由 athrow 指令來實現,除了用 throw 語句顯示拋出的異常以外,Java 虛擬機規范還規定了許多會在 Java 虛擬機檢查到異常狀況時自動拋出的運行時異常。
如:在整數運算中,當除數為 0 時,虛擬機會在 idiv 或 ldiv 指令中拋出 ArithmeticException 異常。
此處需要注意的是,在 Java 虛擬機中處理異常(catch 語句)不是由字節碼指令實現的,而是采用異常處理器(異常表)來完成的。
同步指令
Java 虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,兩種同步都是使用管程(Monitor)來支持的。
- 方法級的同步
方法級的同步時隱式的,即無需通過字節碼指令控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表中 ACC_SYNCHRONIZED 訪問標志得知此方法是否聲明為同步方法。
當方法調用時,如果此方法為同步方法,則執行線程就要去先成功持有管程,然后才能執行方法,方法(無論是否正常完成)完成后釋放管程。
如果這個同步方法執行期間拋出異常,并且方法內部無法處理,那么此方法持有的管程將在異常拋出去后自動釋放。
- 指令序列級的同步
同步一段指令序列通常是由 Java 中的 synchronized 語句塊來表示的,Java 虛擬機指令集中有 monitorenter 和 monitorexit 兩條指令來支持 synchronized 關鍵字。
附錄
- 虛擬機字節碼指令表
參考資料
- 《深入理解 Java 虛擬機》
- 《Java 虛擬機規范 SE 8 版》
我的 GitHub
github.com/jeanboydev
我的公眾號
歡迎關注我的公眾號,分享各種技術干貨,各種學習資料,職業發展和行業動態。
技術交流群
歡迎加入技術交流群,來一起交流學習。
總結
- 上一篇: 使用STM32或GD32解析xml格式数
- 下一篇: ETL工具Kettle使用教程