當前位置:網站首頁>JVM(十七) -- 字節碼與類的加載(二) -- 字節碼指令集

JVM(十七) -- 字節碼與類的加載(二) -- 字節碼指令集

2022-05-15 02:48:12leo_messi94

5. 字節碼指令集

Java字節碼對於虛擬機,就好像匯編語言對於計算機,屬於基本執行命令
Java虛擬機的指令由一個字節長度的,代錶著某種特定操作含義的數字(稱為操作碼:Opcode)以及跟隨其後的零至多個代錶此操作所需參數(稱為操作數:Operands)構成
由於Java虛擬機采用面向操作數棧而不是寄存器的結構,所以大多數的指令都不包含操作數,只有一個操作碼由於限制了Java虛擬機操作碼的長度為一個字節(即0 ~ 255),這意味著指令集的操作碼總數不可能超過256條

執行模型
如果不考慮异常處理的話,那麼Java虛擬機的解釋器可以使用下面這個偽代碼當做最基本的執行模型來理解
在這裏插入圖片描述

5.1 字節碼與數據類型

在Java虛擬機的指令集中,大多數的指令都包含了其操作所對應的數據類型信息
例如,iload指令用於從局部變量錶中加載int類型的數據到操作數棧中,而fload指令加載的則是float類型的數據

對於大部分與數據類型相關的字節碼指令,它們的操作碼助記符中都有特殊的字符來錶明專門為哪種數據類型服務:
大多數對於boolean、byte、short和char類型數據的操作,實際上都是使用相應的int類型作為運算類型

  • i 代錶對int類型的數據進行操作
  • l 代錶long
  • s 代錶short
  • b 代錶 byte
  • c 代錶char
  • f 代錶float
  • d 代錶double

5.2 指令的分類

  • 加載與存儲指令
  • 算術指令
  • 類型轉換指令
  • 方法調用與返回指令
  • 操作數棧管理指令
  • 控制轉移指令
  • 异常處理指令
  • 同步控制指令

5.2.1 加載與存儲指令

作用
加載和存儲指令用於將數據從棧幀的局部變量錶和操作數棧之間來回傳遞

常用指令
1[局部變量壓棧指令]將一個局部變量加載到操作數棧:
xload、xload_(其中x為i、l、f、d
a,n0為到3)
2[常量入棧指令] 將一個常量加載到操作數棧:
bipush、 sipush、ldc、ldc_w、ldc2_w、
aconst_null、 iconst_m1、 iconst_、lconst、
fconst_、 dconst
3[出棧裝入局部變量錶指令]將一個數值從操作數棧存儲到局部變量錶:
xstore、 xstore_(其中x為i、l、f、d、a,n為0到3);
xastore(其中x為、l、f、d、a、b、c、s)
4 擴充局部變量錶的訪問索引的指令:wide
上面所列舉的指令助記符中,有一部分是以尖括號結尾的
(例如iload_)。這些指令助記符實際上代錶了一組指令
(例如iloa_代錶了iload_0、iload_1、iload_2和 iload_3
這幾個指令)。
這幾組指令都是某個帶有一個操作數的通用指令(例如iload)的
特殊形
式,對於這若幹組特殊指令來說,它們錶面上沒有操作數,不需要
進行取操作數的動作,但操作數都隱含在指令中。
比如:
iload_0:將局部變量錶中索引為0比特置上的數據壓入操作數棧中。
除此之外,它們的語義與原生的通用指令完全一致(例如iload_0
的語義與操作數為0時的iload指令語義完全一致。在尖括號之間的
字母指定了指令隱含操作數的數據類型,代錶非負的整數,
くi>代錶是int類型數據,代錶long類型,代錶float類型,
代錶 double類型。

5.2.1.1 局部變量壓棧指令

局部變量壓棧指令將給定的局部變量錶中的數據壓入操作數棧
這類指令大體可以分為
x1oad_(x為i、l、f、d、a,n0為到3)
xload(x為i、l、f、d、a)
說明:在這裏,x的取值錶示數據類型。
指令xload_n錶示將第n個局部變量壓入操作數棧,比如iload_1、
fload_0、 aload_0等指令。其中 aload_n錶示將個對象引用壓棧。
指令xload通過指定參數的形式,把局部變量壓入操作數棧,當使用
這個命令時,錶示局部變量的數量可能超過了4個,比如指令iload、
fload等。

5.2.1.2 常量入棧指令

常量入棧指令的功能是將常數壓入操作數棧,根據數據類型和入
棧內容的不同,又可以分為const系列、push系列和ldc指令。
指令const系列:用於對特定的常量入棧,入棧的常量隱含在指
令本身裏。指令有: iconst_(i從-1到5)、l
const_(l從0到1)、 fconst_(f從0到2)、
dconst_(d從到1)、 aconst_null。

比如,
iconst_m1將-1壓入操作數棧:
iconst_x(x為0到5)將x壓入棧
lconst_0、lconst_1分別將長整數0和1壓入棧:
fconst_0、 fconst_1、 fconst_2分別將浮點數0、1、2壓入棧
dconst_0和 dconst_1分別將double型0和1壓入棧
aconst_null將null壓入操作數棧
從指令的命名上不難找出規律,指令助記符的第一個字符總是
喜歡錶示數據類型,i錶示整數,l錶示長整數,f錶示浮點數,d錶示
雙精度浮點,習慣上用a錶示對象引用。如果指令隱含操作的參數,
會以下劃線形式給出。
指令push系列:主要包括 bipusha和 sipush。它們的區別在於
接收數據類型的不同, bipasha接收8比特整數作為參數,sipush
接收16比特整數,它們都將參數壓入棧。
指令ldc系列:如果以上指令都不能滿足需求,那麼可以使用萬能
的ldc指令,它可以接收一個8比特的參數,該參數指向常量池中的
int、float或者 Stringl的索引,將指定的內容壓入堆棧。

在這裏插入圖片描述

5.2.1.3 出棧裝入局部變量錶指令

出棧裝入局部變量錶指令用於將操作數棧中棧頂元素彈出後,
裝入局部變量錶的指定比特置,用於給局部變量賦值。
這類指令主要以store的形式存在,比如
xstore(x為i、l、f、d、a)、xstore_n(x為i、l、f、d、a,n為0至3).
其中,指令istore n將從操作數棧中彈出一個整數,並把它賦值
給局部變量索引n比特置。
指令xstore由於沒有隱含參數信息,故需要提供一個byte類型的參數
類指定目標局部變量錶的比特置。
說明:
一般說來,類似像 store這樣的命令需要帶一個參數,用來指明
將彈出的元素放在局部變量錶的第幾個比特置。但是,為了盡可能
壓縮指令大小,使用專門的 istore_1指令錶示將彈出的元素放置在
局部變量錶第1個比特置。類似的還有
istore_0、 istore_2、 istore_3,它們分別錶示從操作數棧頂彈出
一個元素,存放在局部變量錶第0、2、3個比特置。

由於局部變量錶前幾個比特置總是非常常用,因此這種做法雖
然増加了指令數量,但是可以大大壓縮生成的字節碼的體積。如
果局部變量錶很大,需要存儲的槽比特大於3,那麼可以使用 istore指
令,外加一個參數,用來錶示需要存放的槽比特比特

5.2.2 算術指令

算術指令用於對兩個操作數梭上的值進行某種特定運算,並把結
果重新壓入操作數。

分類
大體上算術指令可以分為兩種:對整型數據進行運算的指令與對浮
直類型數據進行運算的指令。

byte、 short、char和 boolean類型說明
在每一大類中,都有針對Java虛擬機具體數據類型的專用算術指令。
但沒有直接支持byte、 short、char和 boolean類型的算術指令,
對於這些數據的運算,都使用int類型的指令來處理。此外,
在處理 boolean、byte、 short和char類
型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理。
在這裏插入圖片描述
運算時的溢出
數據運算可能會導致溢出,例如兩個很大的正整數相加,結果可
能是一個負數。其實Java虛擬機規範並無明確規定過整型數據
溢出的具體結果,僅規定了在處理整型數據時,只有除法指令以
及求餘指令中當出現除數為0時會導致虛擬機拋出
异常 Arithmeticexception

運算模式
向最接近數舍入模式:JVM要求在進行浮點數計算時,所有的運
算結果都必須舍入到適當的精度,非精確結果必須舍入為可被
錶示的最接近的精確值,如果有兩種可錶示的形式與該值一樣
接近,將優先選擇最低有效比特為零的;
向零舍入模式:將浮點數轉換為整數時,采用該模式,該模式
將在目標數值類型中選擇一個最接近但是不大於原值的數字作
為最精確的舍入結果

NaN值使用
當一個操作產生溢出時,將會使用有符號的無窮大錶示,如果
某個操作結果沒有明確的數學的話,將會使用NaN值來錶示。而
且所有使用NaN值作為操作數的算術操作,結果都會返回NaN

所有的算術指令包括:
加法指令:iadd ladd、fadd、dadd
减去指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求餘指令:irem、lrem、frem、drem // remainder:餘數
取反指令:ineg、lneg、fneg、dneg // negation:取反
自增指令:iinc
比特運算指令,又可分為
比特移指令:ishl、ishr、 iushr、lshl、lshr、lushr
按比特或指令:ior、lor
按比特與指令:iand、land
按比特异或指令:ixor、lxor
比較指令: dcmpg、dcmpl、 fcmpg、fcmpl、lcmp

5.2.2.1 比較指令

比較指令的作用是比較棧頂兩個元素的大小,並將比較結果入棧
比較指令有: dcmpg,dcmpl、 fcmpg、 fcmpl、lcmp
與前面講解的指令類似,首字符d錶示 double類型,f錶示float,
l錶示long對於double和float類型的數字,由於NaN的存在,各有
兩個版本的比較指令。以 float為例,有 fcmp和fcmpl兩個指令,
它們的區別在於在數字比較時,若遇到NaN值,處理結果不同。
指令dcmpl和 dcmpg也是類似的,根據其命名可以推測其含義,
在此不再贅述。
指令lcmp針對long型整數,由於ong型整數沒有NaN值,
故無需准備兩套指令。
舉例
指令fcmpg和fcmpl都從棧中彈出兩個操作數,並將它們
做比較,設棧頂的元素為v2,棧頂順比特第2比特的元素為v1,若v1=v2,
則壓入0:若v1>v2則壓入1:若v1<v2則壓入-1。
兩個指令的不同之處在於,如果遇到NaN值, fcmpg會壓入1,
而fcmpl會壓入-1。

5.2.3 類型轉換指令

類型轉換指令可以將兩種不同的數值類型進行相互轉換。
這些轉換操作一般用於實現用戶代碼中的顯式類型轉換操作,
或者用來處理字節碼指令集中數據類型相關指令無法與
數據類型一一對應的問題。

5.2.3.1 寬化類型轉換

寬化類型轉換( Widening Numeric Conversions)
1 轉換規則
Java虛擬機直接支持以下數值的寬化類型轉換
( widening numeric conversion,小範圍類型向大範圍類型
的安全轉換)。
也就是說,並不需要指令執行,包括從int類型到long、float
或者 double類型。對應的指令為:i21、i2f、i2d
從long類型到float、 double類型。對應的指令為:i2f、i2d
從float類型到double類型。對應的指令為:f2d
簡化為:int–>long–>float-> double
2 精度損失問題
2.1 寬化類型轉換是不會因為超過目標類型最大值而丟失信息的,
例如,從int轉換到long,或者從int轉換到double,都不會丟失任
何信息,轉換前後的值是精確相等的。
2.2 從int、long類型數值轉換到float,或者long類型數值轉換到
double時,將可能發生精度丟失一一可能丟失掉幾個最低有效
比特上的值,轉換後的浮點數值是根據IEEE754最接近含入模式
所得到的正確整數值。

盡管寬化類型轉換實際上是可能發生精度丟失的,但是這種轉換永
遠不會導致Java虛擬機拋出運行時异常

3 補充說明
從byte、char和 short類型到int類型的寬化類型轉換實際上
是不存在的。對於byte類型轉為int,擬機並沒有做實質性的
轉化處理,只是簡單地通過操作數棧交換了兩個數據。而
將byte轉為long時,使用的是i2l,可以看到在內部
byte在這裏已經等同於int類型處理,類似的還有 short類型,
這種處理方式有兩個特點:
一方面可以减少實際的數據類型,如果為 short和byte都准備
一套指令,那麼指令的數量就會大増,而虛擬機目前的設計
上,只願意使用一個字節錶示指令,因此指令總數不能超
過256個,為了節省指令資源,將 short和byte當做
int處理也在情理之中。
另一方面,由於局部變量錶中的槽比特固定為32比特,無論是byte或
者 short存入局部變量錶,都會占用32比特空間。從這個角度說,
也沒有必要特意區分這幾種數據類型。

5.2.3.2 窄化類型轉換

窄化類型轉換( Narrowing Numeric Conversion)
1 轉換規則
Java虛擬機也直接支持以下窄化類型轉換:
從主int類型至byte、 short或者char類型。對應的指令有:
i2b、i2c、i2s
從long類型到int類型。對應的指令有:l2i
從float類型到int或者long類型。對應的指令有:f2i、f2l
從double類型到int、long或者float類型。對應的指令有:
d2i、d2l、d2f

2 精度損失問題
窄化類型轉換可能會導致轉換結果具備不同的正負號、不同的數量
級,因此,轉換過程很可能會導致數值丟失精度。
盡管數據類型窄化轉換可能會發生上限溢出、下限溢出和精度丟
失等情况,但是Java虛擬機規範中明確規定數值類型的窄化轉換指
令永遠不可能導致虛擬機拋出運行時异常

3 補充說明
3.1 當將一個浮點值窄化轉換為整數類型T(T限於int或long類
型之一)的時候,將遵循以下轉換規則:
如果浮點值是NaN,那轉換結果就是int或long類型的0.
如果浮點值不是無窮大的話,浮點值使用IEEE754的向零含入模
式取整,獲得整數值Vv如果v在目標類型T(int或long)的錶示範圍
之內,那轉換結果就是v。否則,將根據v的符號,轉換為T所
能錶示的最大或者最小正數

3.2 當將一個double類型窄化轉換為float類型時,將遵循
以下轉換規則
通過向最接近數舍入模式舍入一個可以使用float類型錶示
的數字。最後結果根據下面這3條規則判斷
如果轉換結果的絕對值太小而無法使用float來錶示,將返回float類
的正負零
如果轉換結果的絕對值太大而無法使用float來錶示,將返回float類
型的正負無窮大。
對於double類型的NaN值將按規定轉換為float類型的NaN值。

5.2.4 對象的創建與訪問指令

對象的創建與訪問指令
Java是面向對象的程序設計語言,虛擬機平臺從字節碼層
面就對面向對象做了深層次的支持。有一系列指令專門用於對
象操作,可進一步細分為創建指令、字段訪問指令、數組操作
指令、類型檢查指令。

5.2.4.1 創建指令

雖然類實例和數組都是對象,但Java虛擬機對類實例和數組的創
建與操作使用了不同的字節碼指令:
1 創建類實例的指令:
創建類實例的指令 new
它接收一個操作數,為指向常量池的索引,錶示要創建的類型,
執行完成後,將對象的引用壓入棧。
2 創建數組的指令:
創建數組的指令: newarray、 anewarray、 multianewarray.
newarray:創建基本類型數組
anewarray:創建引用類型數組
multilanewarra/創建多維數組
上述創建指令可以用於創建對象或者數組,由於對象和數組在Java中
的廣泛使用,這些指令的使用頻率也非常高。

5.2.4.2 字段訪問指令

對象創建後,就可以通過對象訪問指令獲取對象實例或數組實例
中的字段或者數組元素。

訪問類字段( static字段,或者稱為類變量)的指令:
getstatic、 putstatic
訪問類實例字段(非 static字段,或者稱為實例變量)的指令:
getfield、 putfield

舉例:
以 getstatic指令為例,它含有一個操作數,為指向常量池
的Fieldref索引,它的作用就是獲取 Fieldref指定的
對象或者值,並將其壓入操作數棧。
public void sayhello(){
System. out.println(“hello”);
}
對應的字節碼指令:
0 getstatic #8 <java/lang/System. out>
3 1dc #9
5 invokevirtual #10 <java/io/Printstream println>
8 return

5.2.5.3 數組操作指令

數組操作指令主要有: xastore和 xload指令。具體為:
把一個數組元素加載到操作數棧的指令:
baload、 caload、 saload、 iaload、laload、 faload、
daload、 aaload
將一個操作數棧的值存儲到數組元素中的指令:
bastore、 castore、 sastore、 iastore、lastore、
faster、 dastore、 aastore
即:
在這裏插入圖片描述
取數組長度的指令: arraylength
該指令彈出棧頂的數組元素,獲取數組的長度,將長度壓入棧。
說明
指令xaload錶示將數組的元素壓棧,比如saload、 caload分
別錶示壓入 short數組和char數組。指令
xaload在執行時,要求操作數中棧頂元素為數組索引i,棧頂順
比特第2個元素為數組引用a,該指令會彈出棧頂這
兩個元素,並將a[i]重新壓入堆棧。
xastor則專門針對數組操作,以 iastore為例,它用於
給一個int數組的給定索引賦值。在 iastore執行前
操作數棧頂需要以此准備3個元素:值、索引、數組引用,
iastore會彈出這3個值,並將值賦給數組中指定索
引的比特置。

5.2.4.4 類型檢查指令

檢查類實例或數組類型的指令: instanceof、 checkcast.
指令checkcast用於檢查類型强制轉換是否可以進行。如果
可以進行,那麼checkcast指令不會改變操作數棧
否則它會拋出ClassCasteException异常。
指令 instanceof用來判斷給定對象是否是某一個類的實例,
它會將判斷結果壓入操作數棧。

5.2.5 方法調用與返回指令

5.2.5.1 方法調用指令

方法調用指令: invokevirtual、 invokeinterface、
invokespecial、 invokestatic、 invokedynamic
以下5條指令用於方法調用:

invokeqlvirtual指令用於調用對象的實例方法,根據對象的實際類
型進行分派(虛方法分派),支持多態。這也是Java語言中最
常見的方法分派方式。
invokeinterface指令用於調用接口方法,它會在運行時搜索由特
定對象所實現的這個接口方法,並找出適合的方法進行調用
invokespecia指令用於調用一些需要特殊處理的實例方法,包括
實例初始化方法(構造器)、私有方法和父類方法。這些方法都
是靜態類型綁定的,不會在調用時進行動態派發。
invokestatic指令用於調用命名類中的類方法( static方法)。
這是靜態綁定的。
invokedynamic:調用動態綁定的方法,這個是JDK1.7後新加
入的指令。用於在運行時動態解析出調用點限定符所
引用的方法,並執行該方法。前面4條調用指令的分派邏輯
都固化在java虛擬機內部,而
invokedynamic指令的分派邏輯是由用戶所設定的引導方法决定的。

5.2.5.2 方法返回指令

方法調用結束前,需要進行返回。方法返回指令是根據返
回值的類型區分的。
包括 ireturn(當返回值是 boolean、byte、char、
short和int類型時使用)、lreturn、 freturn、dreturn
和areturn另外還有一條 return指令供聲明為void的方法、實例初
始化方法以及類和接口的類初始化方法使用。
在這裏插入圖片描述
舉例:
通過 ireturn指令,將當前函數操作數棧的頂層元素彈出,並將這
個元素壓入調用者函數的操作數棧中(因為調用者
非常關心函數的返回值),所有在當前函數操作數棧中的其他
元素都會被丟弃。
如果當前返回的是 synchronized方法,那麼還會執行一個隱
含的 monitorexit指令,退出臨界區。

5.2.5 操作數棧管理指令

如同操作一個普通數據結構中的堆棧那樣,JVM提供的操作數棧管理
指令,可以用於直接操作操作數棧的指令。

這類指令包括如下內容
將一個或兩個元素從棧頂彈出,並且直接廢弃:pop,pop2
複制棧頂一個或兩個數值並將複制值或雙份的複制值重新壓入棧頂:
dup,dup2,dup_×1,
dup2_x1, dup x2, dup2 x2;

將棧最頂端的兩個Slot數值比特置交換:swap。Java虛擬機沒有提供
交換兩個64比特數據類型(long、 double)數值的指令
指令nop,是一個非常特殊的指令,它的字節碼為0x00。和
匯編語言中的nop一樣,它錶示什麼都不做。這條指令一般可
用於調試、占比特等。

這些指令屬於通用型,對棧的壓入或者彈出無需指明數據類型。

說明:
不帶_x的指令是複制棧頂數據並壓入棧頂。包括兩個指令,
dup和dup2.dup的系數代錶要複制的Slot個數。

dup開頭的指令用於複制1個Slot的數據。例如1個int或1個
reference類型數據
dup2開頭的指令用於複制2個Slot的數據。例如1個long,或2個int,
或1個int+1個
float類型數據

帶_x的指令是複制棧頂數據並插入棧頂以下的某個比特置。共有4個指令,
dup_x1,dup2_x1,dup_x2,dup2_x2,對於帶_x的複制插入指令,
只要將指令的dup和x的系數相加,結果即為需要插入得比特置
因此
dup_x1插入比特置:1+1=2,即頂2個Slot下面
dup_x2插入比特置:1+2=3,即棧頂3個Slot下面
dup2_x1插入比特置:2+1=3,即棧頂3個Slot下面
dup2_x2插入比特置:2+2=4,即棧頂4個Slot下面
pop:將棧頂的1個Slot數值出棧。例如1個 short.類型數值
pop2:將棧頂的2個Slot數值出棧。例如1個double 類型數值,
或者2個int類型值

5.2.6 控制轉移指令

3.2.7.1 比較指今

比較指令的作用是比較占棧頂兩個元素的大小,並將比較結果入栽。
比較指令有: dcmpg,dcmpl、 fcmp、fcmpl、lcmp
與前面講解的指令類似,首字符d錶示double類型,f錶示float,l錶
示long.對於double和float類型的數字,由於NaN的存在,各有兩個
版本的比較指令。以float為例,有fcmpg和fcmpl兩個指令,它
們的區別在於在數字比較時,若遇到NaN值,處理結果不同。
指令dcmpl和 dcmpg也是類似的,根據其命名可以推測其含義,
在此不再贅述。

舉例
指令 fcmp和fcmpl都從中彈出兩個操作數,並將它們做比較,
設棧頂的元素為v2,頂順比特第2比特的元素為v1,若v1=v2,則壓入0:
若v1>v2則壓入1:若v1<v2則壓入-1.
兩個指令的不同之處在於,如果遇到NaN值, fcmpg會壓入1,
而fcmpl會壓入-1

一 條件跳轉指令
條件跳轉指令通常和比較指令結合使用。在條件跳轉指令執行前,
一般可以先用比較指令進行棧頂元素的准備,然後進行條件跳轉。
條件跳轉指令有:
ifeq,iflt,ifle,ifne,ifgt,ifge, ifnull, ifnonnull。這
些指令都接收兩個字節的操作數
,用於計算跳轉的比特置(16比特符號整數作為當前比特置的offset).
它們的統一含義為:彈出棧頂元素,測試它是否滿足某一條件,
如果滿足條件,則跳轉到給定比特置
具體說明:
在這裏插入圖片描述

比較條件跳轉指令

比較條件跳轉指令類似於比較指令和條件跳轉指令的結合體,它將
比較和跳轉兩個步驟合二為一
這類指令有:if_ icmpeg、if_ cmpne、if_ icmplt、
if_ icmpgt、if_ icmple、if_ icmpge、if_ acmped
和if_ acmpne
其中指令助記符加上“if_”後,以字符“i”開頭的指令針對int型整數
操作(也包括 short和byte類型),以字符“a”開
頭的指令錶示對象引用的比較。
具體說明:
在這裏插入圖片描述
這些指令都接收兩個字節的操作數作為參數,用於計算跳轉的比特置。
同時在執行指令時,棧頂需要准備兩個元素進行比較。
指令執行完成後,棧頂的這兩個元素被清空,且沒有任何數據
入栽。如果預設條件成立,則執行跳轉,否則,繼續執行下條語句。

3.2.7.3 多條件分支跳轉指令

多條件分支跳轉指令是專為 switch-case語句設計的,
主要有 tableswitch和lookupswitch

在這裏插入圖片描述
從助記符上看,兩者都是 switch語句的實現,它們的區別
tableswitch要求多個條件分支值是連續的,它內部只存放起始
值和終止值,以及若幹個跳轉偏移量,通過給定的操作數 index,
可以立即定比特到跳轉偏移量比特置,因此效率比較高
指令lookupswitch內部存放著各個離散的case- offset對,每次執
行都要搜索全部的case- offset對,找到匹配的case值,並根據
對應的 offset計算跳轉地址,因此效率較低。
指令tableswitchl的示意圖如下圖所示。由於tableswitch的
case值是連續的,因此只需要記錄最低值和最高值,以及每項對應的
offset偏移量,根據給定的 indext值通過簡單的計算即可直接定
比特到 offset。
在這裏插入圖片描述

3.2.7.4 無條件跳轉指令

目前主要的無條件跳轉指令為goto。指令goto接收兩個字節的操作
數,共同組成一個帶符號的整數,用於指定指令的偏移量
指令執行的目的就是跳轉到偏移暈給定的比特置處。
如果指令偏移量太大,超過雙字節的帯符號整數的範圍,則可以使用
指令goto_w,它和goto有相同的作用,但是它接收4個字節的操作數,
可以錶示更大的地址範圍。
指令jsr、jsr_w、ret雖然也是無條件跳轉的,但主
要用於try-final1y語句,且已經被虛擬機逐漸廢弃,
故不在這裏介紹這兩個指令。
在這裏插入圖片描述

5.2.7 异常處理指令

3.2.8.1 拋出异常指令

(1) athrow指令
在]ava程序中顯示拋出异常的操作( throw語句)都是由 athrow指
令來實現
除了使用 throw語句顯示拋出异常情况之外,JVM規範還規定了許
多運行時异常會在其他]ava虛擬機指令檢測到异常狀况時自動拋出。
例如,在之前介紹的整數運算時,當除數為零時,虛擬機會在idiv或
ldiv指令中拋出
Arithmeticexception异常。
(2)注意
正常情况下,操作數棧的壓入彈出都是一條條指令完成的。唯一的
例外情况是在拋异常時,]ava虛擬機會清除操作數根上
的所有內容,而後將异常實例壓入調用者操作數上。
异常及异常的處理
過程一:异常對象的生成過程-> throw(手動/自動) -->指令: athrow
過程二: 异常的處理:抓拋模型:try-catch-finally --> 使用异常錶

3.2.8.2 异常處理與异常錶

1 處理异常:
在Java虛擬機中,處理异常( catch語句)不是由字節碼指令來實
現的(早期使用jsr、ret指令),而是采用异常錶來完成
2 异常錶
如果一個方法定義了一個try- catch或者try- final1y的异常處
理,就會創建一個异常錶。它包含了每個异常處理或者finally塊
的信息。
异常錶保
存了每個异常處理信息。比如:
起始比特置
結束比特置
程序計數器記錄的代碼處理的偏移地址
被捕獲的异常類在常量池中的索引
當一個异常被拋出時,3wM會在當前的方法裏尋找一個匹配的處理,
如果沒有找到,這個方法會强制結束並彈出當前棧幀,並且异常會重
新拋給上層調用的方法(在調用方法幀)。如果在所有幀彈出前仍
然沒有找到合適的异常處理,這個線程將終止。如果這個异常在最
後一個非守護線程裏拋出,將會導致JVM自己終止,比如這個線程
是個main線程。
不管什麼時候拋出异常,如果异常處理最終匹配了所有异常類型,
代碼就會繼續執行。在這種情况下,如果方法結束後沒有拋出异常,
仍然執行finally塊,在 return前,它直接跳到 finally塊來完成
目標

5.2.8 同步控制指令

1 方法級的同步
方法級的同步:是隱式的,即無須通過字節碼指令來控制,它實
現在方法調用和返回操作之中。虛擬機可以從方法常量池的
方法錶結構中的 ACC SYNCHRONIZED訪問標志得知一個方法是否聲明
為同步方法
當調用方法時,調用指令將會檢査方法的 ACC SYNCHRONIZED訪問標
志是否設置。
如果設置了,執行線程將先持有同步鎖,然後執行方法。最後在方法完
成(無論是正常aa完成還是非正常完成)時釋放同步鎖。

在方法執行期間,執行線程持有了同步鎖,其他任何線程都無法再獲得
同一個鎖。
如果一個同步方法執行期間拋出了异常,並且在方法內部無法處理此异
常,那這個同步方法所持有的鎖將在异常拋到同步方法之外時自動釋放。

舉例
private int i = 0;
public synchronized void add(){
i++;
}
對應的字節碼:
e aload_0;
1 dup
2 getfield #2 < com/atguigu/javal/SynchronizedTest.i>
5 iconst_1
6 iadd
7 putfield #2 (com/atguigu/java1/SynchronizedTest.i>

2 方法內指定指令序列的同步
同步一段指令集序列:通常是由java中的 synchronized語句塊來錶
示的。jvm的指令集有 monitorenter和
monitorexit兩條指令來支持 synchronized關鍵字的語義。
當一個線程進入同步代碼壩時,它使用 monitorenter指令請求進入。
如果當前對象的監視器計數器為0,則它會被准許進入,若為1,則判斷
持有當前監視器的線程是否為自己,如果是,則進入,否則進行等待,
直到對象的監視器計數器為0,才會被允許進入同步塊。
當線程退出同步塊時,需要使用monitorexiti聲明退出。在Java虛
擬機中,任何對象都有一個監視器與之相關聯,用來判斷對象是否
被鎖定,當監視器被持有後,對象處於鎖定狀態。
指令monitorenter和 monitorexit在執行時,都需要在操作數棧
頂壓入對象,之後 monitorenter和 monitorexitl的鎖定和釋放
都是針對這個對象的監視器進行的。
下圖展示了監視器如何保護臨界區代碼不同時被多個線程訪問,
只有當線程4離開臨界區後,線程1、2、3才有可能進入。
在這裏插入圖片描述

版權聲明
本文為[leo_messi94]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/135/202205142122403005.html

隨機推薦