您當前的位置:首頁 > 攝影

位元組碼指令簡介

作者:由 ppppppkun 發表于 攝影時間:2022-10-23

前言

Java虛擬機器的指令均是一個位元組長度的,代表某種特定操作含義的操作碼和運算元組成(像組合語言那樣)。

Java虛擬機器採用面向運算元棧而不是面向暫存器的架構,所以大多數指令都不包含運算元,只有一個操作碼,指令引數都放在運算元棧中。

由於限制了Java虛擬機器操作碼的長度為一個位元組(0~255),這意味著指令集的操作碼總數不能夠超過256條。同時因為編譯後沒用運算元長度對齊,導致虛擬機器在處理超過一個位元組的資料時需要在執行時從位元組中重建出具體資料的結構。

栗子:將一個16位長度的無符號整數使用的兩個無符號位元組儲存起來,那麼這個整數的值應該是

(byte1 << 8) | byte 2

這導致了虛擬機器效能的降低,但是也存在一定的優勢,那就是我們得到了儘可能短小精幹的編譯程式碼。JVM執行的模型和計算機CPU取指令執行的模型是一樣的。

位元組碼和資料型別

JVM的指令集中大多數指令都包含其操作所對應的資料型別資訊。

栗子:iload指令用於從區域性變量表中載入int型的資料到運算元棧中。fload則是float型別。

這兩條指令的操作在虛擬機器內部可能會由同一段程式碼來實現。但在class檔案中它們必須擁有各自獨立的操作碼。

i代表對int型別的資料操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

位元組碼指令簡介

位元組碼指令簡介

透過使用資料型別列所代表的特殊字元替換opcode列的指令模板T,就可以得到一個具體的位元組碼指令。(空表示不支援)

大多數對於boolean、byte、short和char型別資料的操作,實際上都是使用相應的對int型別作為運算型別(Computational Type)來進行的。

載入和儲存指令

將一個區域性變數載入到運算元棧:Tload

將一個數值從運算元棧儲存到區域性變量表:Tstore

將一個常數載入到運算元棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst

、lconst

、fconst

、dconst

擴充區域性變量表:wide

還有一些如訪問物件的欄位或陣列元素的指令也會向運算元棧傳輸資料。

帶尖括號的指令,比如iload_,實際上表示了iload_0,iload_1,iload_2,iload_3這幾條指令,它們省略掉了顯式的運算元,不需要進行取運算元的動作。iload_0的語義和運算元為0時的iload指令完全一致

運算指令

算術指令用於堆兩個運算元棧上的值進行特定的運算,並把結果重新存入棧頂。大體上運算指令可以分為兩種:對整數和對浮點數。JVM裡面時不存在直接支援byte、short、char和boolean型別的算術指令,對於上述幾種資料的運算,應使用操作int型別的指令代替。

add。 sub, mul, div。 rem(求餘), neg(取反),位移指令, or, and, xor, inc(區域性變數自增), 比較指令

只有除法指令(idiv和ldiv)以及求餘指令(irem和lrem)中當出現除數為零時會導致虛擬機器丟擲ArithmeticException異常,其餘任何整型數運算場景都不會丟擲執行時異常。

Java虛擬機器必須完全支援IEEE 754中定義的“非正規浮點數值”(DenormalizedFloating-Point Number)和“逐級下溢”(Gradual Underflow)的運算規則。Java虛擬機器在處理浮點數運算時,不會丟擲任何執行時異常。

在對long型別數值進行比較時,Java虛擬機器採用帶符號的比較方式。

型別轉換指令

Java虛擬機器直接支援寬化型別轉換

小範圍型別向大範圍型別的安全轉換

int -> long, float, double

long -> float, double

float -> double

窄化型別轉換需要顯示的使用指令完成:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

窄化型別轉換可能會導致轉換結果產生不同的正負號,不同的數量級的情況,常常導致精度丟失

在將int或long型別窄化轉換為整數型別T的時候,轉換過程僅僅是簡單丟棄除最低位N位元組以外的內容,N是型別T的資料型別長度,這將可能導致轉換結果與輸入值有不同的正負號。

JVM將浮點型窄化為整數型的時候,如果浮點數是NAN,那整形就是0,如果浮點數不是正無窮大,那就向零舍入取整,如果超過了整形的範圍,就轉換成整型所能表示的最接近的數

窄化轉換指令永遠不可能導致虛擬機器丟擲執行時異常。

物件建立與訪問指令

Java虛擬機器對類例項和陣列的建立與操作使用了不同的位元組碼指令

建立類:new

建立陣列:newarray,anewarray,multianewarray

訪問類欄位和例項欄位:getfield,putfield,getstatic,putstatic

將陣列載入到運算元棧:Taload

將運算元棧的儲存到陣列元素中:Tastore

取陣列長度:arraylength

檢查類實力型別:instanceof,checkcast

運算元棧管理指令

出棧:pop,pop2

複製棧頂的一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、duo_x1、dup2_x2、dup_x2

將棧最頂端的兩個數值互換:swap

控制轉移指令

具體指令略

在Java虛擬機器中有專門的指令集用來處理int和reference型別的條件分支比較操作,為了可以無須明顯標識一個數據的值是否null,也有專門的指令用來檢測null值。

cmp會先執行運算指令,然後將一個整型值返回到運算元棧中,隨後再執行int型的條件分支比較來完成操作跳轉

方法呼叫和返回指令

invokevirtual指令:用於呼叫物件的例項方法,根據物件的實際型別進行分派(虛方法分派)

invokeinterface指令:用於呼叫介面方法,它會在執行時搜尋一個實現了這個介面方法的物件,找出適合的方法進行呼叫。

invokespecial指令:用於呼叫一些需要特殊處理的例項方法,包括例項初始化方法、私有方法和父類方法。

invokestatic指令:用於呼叫類靜態方法(static方法)。

invokedynamic指令:用於在執行時動態解析出呼叫點限定符所引用的方法。

有一條return指令供宣告為void的方法、例項初始化方法、類和介面的類初始化方法使用。

boolean,byte,char,short,int都是ireturn,其他還有lreturn,freturn,dreturn和areturn

異常處理指令

顯示的丟擲異常都由athrow指令實現,JVM中處理異常是由異常表來完成的。

同步指令

JVM支援方法級的同步和方法內部一段指令序列的同步,這兩種同步都是使用管程來實現的。

方法級的同步是隱式的,無須透過位元組碼指令來控制,它實現在方法呼叫和返回操作之中。

當方法呼叫時,呼叫指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設定,如果設定了,執行執行緒就要求先成功持有管程,然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放管程。

如果一個同步方法執行期間丟擲了異常,並且在方法內部無法處理此異常,那這個同

步方法所持有的管程將在異常拋到同步方法邊界之外時自動釋放。

同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的,Java虛擬機器的指令集中有monitorenter和monitorexit兩條指令來支援synchronized關鍵字的語義,正確實現synchronized關鍵字需要Javac編譯器與Java虛擬機器兩者共同協作支援

編譯器必須確保無論方法透過何種方式完成,方法中呼叫過的每條monitorenter指令都必須有其對應的monitorexit指令

參考

[1] 深入理解Java虛擬機器 周志明

標簽: 指令  虛擬機器  運算元  Java  方法