Java全能學習+面試指南:https://javaxiaobear.cn
1、字節碼指令集與解析概述
Java字節碼對于虛擬機,就好像匯編語言對于計算機,屬于基本執行指令。
Java 虛擬機的指令由一個字節長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其后的零至多個代表此操作所需參數(稱為操作數,Operands)而構成。由于 Java 虛擬機采用面向操作數棧而不是寄存器的結構,所以大多數的指令都不包含操作數,只有一個操作碼。
由于限制了 Java 虛擬機操作碼的長度為一個字節(即 0~255),這意味著指令集的操作碼總數不可能超過 256 條。
官方文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
熟悉虛擬機的指令對于動態字節碼生成、反編譯Class文件、Class文件修補都有著非常重要的價值。因此,閱讀字節碼作為了解 Java 虛擬機的基礎技能,需要熟練掌握常見指令。
1、字節碼與數據類型
在Java虛擬機的指令集中,大多數的指令都包含了其操作所對應的數據類型信息。例如,iload指令用于從局部變量表中加載int型的數據到操作數棧中,而fload指令加載的則是float類型的數據。
對于大部分與數據類型相關的字節碼指令,它們的操作碼助記符中都有特殊的字符來表明專門為哪種數據類型服務:
- i代表對int類型的數據操作
- l代表long類型的數據操作
- s代表short類型的數據操作
- b代表byte類型的數據操作
- c代表char類型的數據操作
- f代表float類型的數據操作
- d代表double類型的數據操作
也有一些指令的助記符中沒有明確地指明操作類型的字母,如arraylength指令,它沒有代表數據類型的特殊字符,但操作數永遠只能是一個數組類型的對象。
還有另外一些指令,如無條件跳轉指令goto則是與數據類型無關的。
大部分的指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器會在編譯期或運行期將byte和short類型的數據帶符號擴展(Sign-Extend)為相應的int類型數據,將boolean和char類型數據零位擴展(Zero-Extend)為相應的int類型數據。與之類似,在處理boolean、byte、short和char類型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理。因此,大多數對于boolean、byte、short和char類型數據的操作,實際上都是使用相應的int類型作為運算類型。
byte b1 = 12;
short s1 = 10;
int i = b1 + s1;
2、指令分類
由于完全介紹和學習這些指令需要花費大量時間。為了讓大家能夠更快地熟悉和了解這些基本指令,這里將JVM中的字節碼指令集按用途大致分成 9 類。
- 加載與存儲指令
- 算術指令
- 類型轉換指令
- 對象的創建與訪問指令
- 方法調用與返回指令
- 操作數棧管理指令
- 控制轉移指令
- 異常處理指令
- 同步控制指令
在做值相關操作時:
- 一個指令,可以從局部變量表、常量池、堆中對象、方法調用、系統調用中等取得數據,這些數據(可能是值,可能是對象的引用)被壓入操作數棧。
- 一個指令,也可以從操作數棧中取出一到多個值(pop多次),完成賦值、加減乘除、方法傳參、系統調用等等操作。
2、字節碼指令
1、加載與存儲指令
1、操作數棧
我們知道,Java字節碼是Java虛擬機所使用的指令集。因此,它與Java虛擬機基于棧的計算模型是密不可分的。
在解釋執行過程中,每當為Java方法分配棧楨時,Java虛擬機往往需要開辟一塊額外的空間作為操作數棧,來存放計算的操作數以及返回結果。
具體來說便是:執行每一條指令之前,Java 虛擬機要求該指令的操作數已被壓入操作數棧中。在執行指令時,Java 虛擬機會將該指令所需的操作數彈出,并且將指令的結果重新壓入棧中。
以加法指令 iadd 為例。假設在執行該指令前,棧頂的兩個元素分別為 int 值 1 和 int 值 2,那么 iadd 指令將彈出這兩個 int,并將求得的和 int 值 3 壓入棧中。
由于 iadd 指令只消耗棧頂的兩個元素,因此,對于離棧頂距離為 2 的元素,即圖中的問號,iadd 指令并不關心它是否存在,更加不會對其進行修改。
2、局部變量表(Local Variables)
Java 方法棧楨的另外一個重要組成部分則是局部變量區,字節碼程序可以將計算的結果緩存在局部變量區之中。
實際上,Java 虛擬機將局部變量區當成一個數組,依次存放 this 指針(僅非靜態方法),所傳入的參數,以及字節碼中的局部變量。
和操作數棧一樣,long 類型以及 double 類型的值將占據兩個單元,其余類型僅占據一個單元。
例如:
public void foo(long l, float f) {{int i = 0;}{String s = "Hello, World";}
}
對應的圖示:
在棧幀中,與性能調優關系最為密切的部分就是局部變量表。局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。
在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
3、局部變量壓棧指令
局部變量壓棧指令將給定的局部變量表中的數據壓入操作數棧。
這類指令大體可以分為:
> xload_ (x為i、l、f、d、a,n為 0 到 3)
> xload (x為i、l、f、d、a)
說明:在這里,x的取值表示數據類型。
指令xload_n表示將第n個局部變量壓入操作數棧,比如iload_1、fload_0、aload_0等指令。其中aload_n表示將一個對象引用壓棧。
指令xload通過指定參數的形式,把局部變量壓入操作數棧,當使用這個命令時,表示局部變量的數量可能超過了4個,比如指令iload、fload等。
4、常量入棧指令
常量入棧指令的功能是將常數壓入操作數棧,根據數據類型和入棧內容的不同,又可以分為const系列、push系列和ldc指令
指令const
用于對特定的常量入棧,入棧的常量隱含在指令本身里。指令有:iconst_ (i從-1到5)、lconst_ (l從0到1)、fconst_ (f從0到2)、dconst_ (d從0到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表示對象引用。如果指令隱含操作的參數,會以下劃線形式給出。
int i = 3; iconst_3
int j = 6; iconst 6? bipush 6?
int k = 32768 ldc ?
指令push
主要包括bipush和sipush。它們的區別在于接收數據類型的不同,bipush接收8位整數作為參數,sipush接收16位整數,它們都將參數壓入棧。
指令ldc
如果以上指令都不能滿足需求,那么可以使用萬能的ldc指令,它可以接收一個8位的參數,該參數指向常量池中的int、float或者String的索引,將指定的內容壓入堆棧。類似的還有ldc_w,它接收兩個8位參數,能支持的索引范圍大于ldc。
如果要壓入的元素是long或者double類型的,則使用ldc2_w指令,使用方式都是類似的。
總結如下:
5、出棧裝入局部變量表指令
出棧裝入局部變量表指令用于將操作數棧中棧頂元素彈岀后,裝入局部變量表的指定位置,用于給局部變量賦值。
這類指令主要以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指令,外加一個參數,用來表示需要存放的槽位位置。
2、算術指令
算術指令用于對兩個操作數棧上的值進行某種特定運算,并把結果重新壓入操作數棧
大體上算術指令可以分為兩種:對整型數據進行運算的指令與對浮點類型數據進行運算的指令
byte、short、char和boolean類型說明
在每一大類中,都有針對Java虛擬機具體數據類型的專用算術指令。但沒有直接支持byte、short、char和boolean類型的算術指令,對于這些數據的運算,都使用int類型的指令來處理。此外,在處理boolean、byte、short和char類型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理
運算時的溢出
數據運算可能會導致溢出,例如兩個很大的正整數相加,結果可能是一個負數。其實Java虛擬機規范并無明確規定過整型數據溢出的具體結果,僅規定了在處理整型數據時,只有除法指令以及求余指令中當出現除數為0時會導致虛擬機拋出異常ArithmeticException。
運算模式
- 向最接近數舍入模式:JVM要求在進行浮點數計算時,所有的運算結果都必須舍入到適當的精度,非精確結果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值一樣接近,將優先選擇最低有效位為零的;
- 向零舍入模式:將浮點數轉換為整數時,采用該模式,該模式將在目標數值類型中選擇一個最接近但是不大于原值的數字作為最精確的舍入結果;
NaN值使用
當一個操作產生溢出時,將會使用有符號的無窮大表示,如果某個操作結果沒有明確的數學定義的話,將會使用 NaN值來表示。而且所有使用NaN值作為操作數的算術操作,結果都會返回 NaN;
1、算數指令
-
加法指令: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
例如
public static int bar(int i) {return ((i + 1) - 2) * 3 / 4;
}
字節碼指令對應的圖示:

2、比較指令的說明
- 比較指令的作用是比較棧頂兩個元素的大小,并將比較結果入棧
- 比較指令有:dcmpg, dcmpl、fcmpg、fcmpl、lcmp,與前面講解的指令類似,首字符d表示double類型,f表示float,l表示long
- 對于double和float類型的數字,由于NaN的存在,各有兩個版本的比較指令。以float為例,有fcmpg和fcmpl兩個指令,它們的區別在于在數字比較時,若遇到NaN值,處理結果不同
- 指令dcmpl和dcmpg也是類似的,根據其命名可以推測其含義,在此不再贅述
- 指令lcmp針對long型整數,由于long型整數沒有NaN值,故無需準備兩套指令
舉例:
指令fcmpg和fcmpl都從棧中彈出兩個操作數,并將它們做比較,設棧頂的元素為v2,棧頂順位第2位的元素為v1,若v1=v2,則壓入0;若v1>v2則壓入1;若v1<v2則壓入-1。
兩個指令的不同之處在于,如果遇到NaN值,fcmpg會壓入1,而fcmpl會壓入-1。
數值類型的數據,才可以談大小! (byte\short\char\int;long\float\double),boolean、引用數據類型不能比較大小。
3、類型轉換指令
1、寬化類型轉換
1、轉換規則
Java虛擬機直接支持以下數值的寬化類型轉換(widening numeric conversion,小范圍類型向大范圍類型的安全轉換)。也就是說,并不需要指令執行,包括:
- 從int類型到long、float或者double類型。對應的指令為:i2l、i2f、i2d
- 從long類型到float、double類型。對應的指令為:l2f、l2d
- 從float類型到double類型。對應的指令為:f2d
簡化為:int --> long --> float --> double
2、 精度損失問題
- 寬化類型轉換是不會因為超過目標類型最大值而丟失信息的,例如,從int轉換到 long,或者從int轉換到double,都不會丟失任何信息,轉換前后的值是精確相等的
- 從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位空間。從這個角度說,也沒
有必要特意區分這幾種數據類型。
2、窄化類型轉換(Narrowing Numeric Conversion)
1、轉換規則
Java虛擬機也直接支持以下窄化類型轉換:
- 從int類型至byte、short或者char類型。對應的指令有:i2b、i2s、i2c
- 從long類型到int類型。對應的指令有:l2i
- 從float類型到int或者long類型。對應的指令有:f2i、f2l
- 從double類型到int、long或者float類型。對應的指令有:d2i、d2l、d2f
2、精度損失問題
窄化類型轉換可能會導致轉換結果具備不同的正負號、不同的數量級,因此,轉換過程很可能會導致數值丟失精度。
盡管數據類型窄化轉換可能會發生上限溢出、下限溢出和精度丟失等情況,但是Java虛擬機規范中明確規定數值類型的窄化轉換指令永遠不可能導致虛擬機拋出運行時異常。
3、補充說明
- 當將一個浮點值窄化轉換為整數類型T(T限于int或long類型之一)的時候,將遵循以下轉換規則:
- 如果浮點值是NaN,那轉換結果就是int或long類型的0。
- 如果浮點值不是無窮大的話,浮點值使用IEEE 754的向零舍入模式取整,獲得整數值v,如果v在目標類型T(int或long)的表示范圍之內,那轉換結果就是v。否則,將根據v的符號,轉換為T所能表示的最大或者最小正數
- 當將一個 double 類型窄化轉換為 float 類型時,將遵循以下轉換規則:
通過向最接近數舍入模式舍入一個可以使用float類型表示的數字。最后結果根據下面這3條規則判斷:- 如果轉換結果的絕對值太小而無法使用 float來表示,將返回 float類型的正負零
- 如果轉換結果的絕對值太大而無法使用 float來表示,將返回 float類型的正負無窮大
- 對于double 類型的 NaN值將按規定轉換為 float類型的 NaN值
4、對象的創建與訪問指令
Java是面向對象的程序設計語言,虛擬機平臺從字節碼層面就對面向對象做了深層次的支持。有一系列指令專門用于對象操作,可進一步細分為創建指令、字段訪問指令、數組操作指令、類型檢查指令
1、創建指令
雖然類實例和數組都是對象,但Java虛擬機對類實例和數組的創建與操作使用了不同的字節碼指令
創建類實例的指令
創建類實例的指令:new,它接收一個操作數,為指向常量池的索引,表示要創建的類型,執行完成后,將對象的引用壓入棧
創建數組的指令
創建數組的指令:newarray、anewarray、multianewarray
- newarray:創建基本類型數組
- anewarray:創建引用類型數組
- multianewarray:創建多維數組
上述創建指令可以用于創建對象或者數組,由于對象和數組在Java中的廣泛使用,這些指令的使用頻率也非常高
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 ldc #9
5 invokevirtual #10 <java/io/PrintStream.println>
8 return
圖示如下:
3、數組操作指令
數組操作指令主要有:xastore和xaload指令。具體為
- 把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 將一個操作數棧的值存儲到數組元素中的指令:bastore、 castore、 sastore、iastore、 lastore、fastore、dastore、aastore

- 取數組長度的指令:arraylength
- 該指令彈出棧頂的數組元素,獲取數組的長度,將長度壓入棧
指令xaload表示將數組的元素壓棧,比如saload、caload分別表示壓入short數組和char數組。指令xaload在執行時,要求操作數中棧頂
元素為數組索引i,棧頂順位第2個元素為數組引用a,該指令會彈岀棧頂這兩個元素,并將a[i]重新壓入棧。
xastore則專門針對數組操作,以iastore為例,它用于給一個int數組的給定索引賦值。在iastore執行前,操作數棧頂需要以此準備3個元
素:值、索引、數組引用,iastore會彈出這3個值,并將值賦給數組中指定索引的位置。
4、類型檢查指令
檢查類實例或數組類型的指令:instanceof、checkcast。
- 指令checkcast用于檢查類型強制轉換是否可以進行。如果可以進行,那么checkcast指令不會改變操作數棧,否則它會拋出ClassCastException異常。
- 指令instanceof用來判斷給定對象是否是某一個類的實例,它會將判斷結果壓入操作數棧。
5、方法調用與返回指令
1、調用指令
方法調用指令:invokevirtual、invokeinterface、invokespecial、invokestatic 、invokedynamic
以下5條指令用于方法調用:
- invokevirtual指令用于調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),支持多態。這也是Java語言中最常見的方法分派方式。
- invokeinterface指令用于調用接口方法,它會在運行時搜索由特定對象所實現的這個接口方法,并找出適合的方法進行調用。
- invokespecial指令用于調用一些需要特殊處理的實例方法,包括實例初始化方法(構造器)、私有方法和父類方法。這些方法都是靜態類型綁定的,不會在調用時進行動態派發。
- invokestatic指令用于調用命名類中的類方法(static方法)。這是靜態綁定的。
- invokedynamic:調用動態綁定的方法,這個是JDK 1.7后新加入的指令。用于在運行時動態解析出調用點限定符所引用的法,并執行該方法。前面4條調用指令的分派邏輯都固化在 java 虛擬機內部,而 invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
2、返回指令
方法調用結束前,需要進行返回。方法返回指令是根據返回值的類型區分的
- 包括ireturn(當返回值是 boolean、byte、char、short和int 類型時使用)、lreturn、freturn、dreturn和areturn
- 另外還有一條return 指令供聲明為 void的方法、實例初始化方法以及類和接口的類初始化方法使用。
舉例:
通過ireturn指令,將當前函數操作數棧的頂層元素彈出,并將這個元素壓入調用者函數的操作數棧中(因為調用者非常關心函數的返回值),所有在當前函數操作數棧中的其他元素都會被丟棄。
如果當前返回的是synchronized方法,那么還會執行一個隱含的monitorexit指令,退出臨界區。
最后,會丟棄當前方法的整個幀,恢復調用者的幀,并將控制權轉交給調用者。

對應的代碼:
public int methodReturn(){int i = 500;int j = 200;int k = 50;return (i + j) / k;
}
6、操作數棧管理指令
如同操作一個普通數據結構中的堆棧那樣,JVM提供的操作數棧管理指令,可以用于直接操作操作數棧的指令
這類指令包括如下內容:
- 將一個或兩個元素從棧頂彈出,并且直接廢棄: pop,pop2;
- 復制棧頂一個或兩個數值并將復制值或雙份的復制值重新壓入棧頂: dup, dup2, dup_x1, 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類型數值
7、控制轉移指令
1、條件跳轉指令
條件跳轉指令通常和比較指令結合使用。在條件跳轉指令執行前,一般可以先用比較指令進行棧頂元素的準備,然后進行條件跳轉。
條件跳轉指令有: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull。這些指令都接收兩個字節的操作數,用于計算跳轉的位置(16位符號整數作為當前位置的offset)。
它們的統一含義為:彈出棧頂元素,測試它是否滿足某一條件,如果滿足條件,則跳轉到給定位置。
注意:
- 與前面運算規則一致:
- 對于boolean、byte、char、short類型的條件分支比較操作,都是使用int類型的比較指令完成
- 對于long、float、double類型的條件分支比較操作,則會先執行相應類型的比較運算指令,運算指令會返回一個整型值到操作數棧中,隨后再執行int類型的條件分支比較操作來完成整個分支跳轉
- 由于各類型的比較最終都會轉為 int 類型的比較操作,所以Java虛擬機提供的int類型的條件分支指令是最為豐富和強大的
2、比較條件跳轉指令
比較條件跳轉指令類似于比較指令和條件跳轉指令的結合體,它將比較和跳轉兩個步驟合二為一。
這類指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。其中指令助記符加上“if_”后,以字符“i”開頭的指令針對int型整數操作(也包括short和byte類型),以字符“a”開頭的指令表示對象引用的比較
這些指令都接收兩個字節的操作數作為參數,用于計算跳轉的位置。同時在執行指令時,棧頂需要準備兩個元素進行比較。指令執行完成后,棧頂的這兩個元素被清空,且沒有任何數據入棧。如果預設條件成立,則執行跳轉,否則,繼續執行下一條語句。
3、多條件分支跳轉
多條件分支跳轉指令是專為switch-case語句設計的,主要有tableswitch和lookupswitch
從助記符上看,兩者都是switch語句的實現,它們的區別:
- tableswitch要求多個條件分支值是連續的,它內部只存放起始值和終止值,以及若干個跳轉偏移量,通過給定的操作數index, 可以立即定位到跳轉偏移量位置,因此效率比較高
- 指令lookupswitch內部存放著各個離散的case-offset對,每次執行都要搜索全部的case-offset對,找到匹配的case值,并根據對應的offset計算跳轉地址,因此效率較低
指令tableswitch的示意圖如下圖所示。由于tableswitch的case值是連續的,因此只需要記錄最低值和最高值,以及每一項對應的offset偏移量,根據給定的index值通過簡單的計算即可直接定位到offset。
指令lookupswitch處理的是離散的case值,但是出于效率考慮,將case-offset對按照case 值大小排序,給定index時,需要査找與index相等的case,獲得其offset,如果找不到則跳轉到default。指令lookupswitch 如下圖所示。
4、無條件跳轉
目前主要的無條件跳轉指令為goto。指令goto接收兩個字節的操作數,共同組成一個帶符號的整數,用于指定指令的偏移量,指令執行的目的就是跳轉到偏移量給定的位置處。
如果指令偏移量太大,超過雙字節的帶符號整數的范圍,則可以使用指令goto_w,它和goto有相同的作用,但是它接收4個字節的操作數,可以表示更大的地址范圍。
指令jsr、jsr_w、ret雖然也是無條件跳轉的,但主要用于 try-finally語句,且已經被虛擬機逐漸廢棄,故不在這里介紹這兩個指令。
8、異常處理指令
1、拋出異常指令
athrow指令
在Java程序中顯示拋出異常的操作(throw語句)都是由athrow指令來實現。
除了使用throw語句顯示拋出異常情況之外,JVM規范還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。例
如,在之前介紹的整數運算時,當除數為零時,虛擬機會在 idiv或 ldiv指令中拋出 ArithmeticException異常。
注意
正常情況下,操作數棧的壓入彈出都是一條條指令完成的。唯一的例外情況是在拋異常時,Java 虛擬機會清除操作數棧上的所有內容,而后將異常實例壓入調用者操作數棧上
異常及異常的處理:
過程一:異常對象的生成過程 —> throw (手動 / 自動) —> 指令:athrow
過程二:異常的處理:抓拋模型。 try-catch-finally —> 使用異常表
2、異常處理與異常表
1、處理異常
在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的(早期使用jsr、ret指令),而是采用異常表來完成的
2、異常表
如果一個方法定義了一個try-catch 或者try-finally的異常處理,就會創建一個異常表。它包含了每個異常處理或者finally塊的信息。異常表保存了每個異常處理信息。比如:
- 起始位置
- 結束位置
- 程序計數器記錄的代碼處理的偏移地址
- 被捕獲的異常類在常量池中的索引
當一個異常被拋出時,JVM會在當前的方法里尋找一個匹配的處理,如果沒有找到,這個方法會強制結束并彈出當前棧幀,并且異常會重新拋給上層調用的方法(在調用方法棧幀)。如果在所有棧幀彈出前仍然沒有找到合適的異常處理,這個線程將終止。如果這個異常在最后一個非守護線程里拋出,將會導致JVM自己終止,比如這個線程是個main線程。
不管什么時候拋出異常,如果異常處理最終匹配了所有異常類型,代碼就會繼續執行。在這種情況下,如果方法結束后沒有拋出異常,仍然執行finally塊,在return前,它直接跳到finally塊來完成目標
9、同步控制指令
1、方法級的同步
方法級的同步:是隱式的, 即無須通過字節碼指令來控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表結構中的 ACC_SYNCHRONIZED 訪問標志得知一個方法是否聲明為同步方法;
當調用方法時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否設置。
- 如果設置了,執行線程將先持有同步鎖,然后執行方法。最后在方法完成(無論是正常完成還是非正常完成)時釋放同步鎖。
- 在方法執行期間,執行線程持有了同步鎖,其他任何線程都無法再獲得同一個鎖。
- 如果一個同步方法執行期間拋出了異常,并且在方法內部無法處理此異常,那這個同步方法所持有的鎖將在異常拋到同步方法之外時自動釋放。
例如:
private int i = 0;
public synchronized void add(){i++;
}
對應的字節碼:
0 aload_0
1 dup
2 getfield #2 <com/atguigu/java1/SynchronizedTest.i>
5 iconst_1
6 iadd
7 putfield #2 <com/atguigu/java1/SynchronizedTest.i>
10 return
說明:
這段代碼和普通的無同步操作的代碼沒有什么不同,沒有使用monitorenter和monitorexit進行同步區控制。這是因為,對于同步方法而
言,當虛擬機通過方法的訪問標示符判斷是一個同步方法時,會自動在方法調用前進行加鎖,當同步方法執行完畢后,不管方法是正常結
束還是有異常拋岀,均會由虛擬機釋放這個鎖。因此,對于同步方法而言,monitorenter 和monitorexit指令是隱式存在的,并未直接出
現在字節碼中。
2、方法內指定指令序列的同步
同步一段指令集序列:通常是由java中的synchronized語句塊來表示的。jvm的指令集有 monitorenter 和 monitorexit 兩條指令來支持 synchronized關鍵字的語義。
當一個線程進入同步代碼塊時,它使用monitorenter指令請求進入。如果當前對象的監視器計數器為0,則它會被準許進入,若為1,則判斷持有當前監視器的線程是否為自己,如果是,則進入,否則進行等待,直到對象的監視器計數器為0,才會被允許進入同步塊。
當線程退岀同步塊時,需要使用monitorexit聲明退出。在Java虛擬機中,任何對象都有一個監視器與之相關聯,用來判斷對象是否被鎖定,當監視器被持有后,對象處于鎖定狀態。
指令monitorenter和monitorexit在執行時,都需要在操作數棧頂壓入對象,之后monitorenter和monitorexit的鎖定和釋放都是針對這個對象的監視器進行的。
下圖展示了監視器如何保護臨界區代碼不同時被多個線程訪問,只有當線程4離開臨界區后,線程1、2、3才有可能進入。
例如:
private int i = 0;public void subtract(){synchronized (this){i--;}
}
對應的字節碼:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: isub
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
編譯器必須確保無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須執行其對應的monitorexit指令,而無論這個方法是正常結束還是異常結束。
為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行monitorexit指令