聽我說溉瓶,聽我說急鳄,學 Java 準沒錯,畢竟崗位多薪資高堰酿!但涌進來的人越多疾宏,就意味著越來越卷,要想不被卷到触创,就必須得瘋狂的學習坎藐,學什么呢?Java 字節(jié)碼指令就是一塊硬骨頭哼绑。
有些讀者可能會有這樣的疑惑岩馍,“Java 字節(jié)碼難嗎?Java 虛擬機難嗎抖韩?我能不能學會爸鳌?”
不要擔心茂浮,有二哥在双谆,保證小白也能看得懂!
講良心話席揽,不是我謙虛佃乘,一開始學 Java 字節(jié)碼和 Java 虛擬機方面的知識我也感覺頭大!但硬著頭皮學了一陣子之后驹尼,突然就開竅了趣避,覺得好有意思,尤其是明白了 Java 代碼在底層竟然是這樣執(zhí)行的時候新翎,感覺既膨脹又飄飄然程帕,渾身上下散發(fā)著自信的光芒住练!
我在 簡書 共輸出了 100 多篇 Java 方面的文章,總字數(shù)超過 30 萬字愁拭, 內(nèi)容風趣幽默讲逛、通俗易懂,收獲了很多初學者的認可和支持岭埠,內(nèi)容包括 Java 語法盏混、Java 集合框架、Java 并發(fā)編程惜论、Java 虛擬機等核心內(nèi)容许赃。
為了幫助更多的 Java 初學者,我“一怒之下”就把這些文章重新整理并開源到了 GitHub馆类,起名《教妹學 Java》混聊,聽起來是不是就很有趣?
GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java
之前的文章里我也提到了乾巧,Java 官方的虛擬機 Hotspot 是基于棧的句喜,而不是基于寄存器的。
基于棧的優(yōu)點是可移植性更好沟于、指令更短咳胃、實現(xiàn)起來簡單,但不能隨機訪問棧中的元素旷太,完成相同功能所需要的指令數(shù)也比寄存器的要多展懈,需要頻繁的入棧和出棧。
基于寄存器的優(yōu)點是速度快泳秀,有利于程序運行速度的優(yōu)化标沪,但操作數(shù)需要顯式指定榄攀,指令也比較長嗜傅。
Java 字節(jié)碼由操作碼和操作數(shù)組成。
- 操作碼(Opcode):一個字節(jié)長度(0-255檩赢,意味著指令集的操作碼總數(shù)不可能超過 256 條)吕嘀,代表著某種特定的操作含義。
- 操作數(shù)(Operands):零個或者多個贞瞒,緊跟在操作碼之后偶房,代表此操作需要的參數(shù)。
由于 Java 虛擬機是基于棧而不是寄存器的結(jié)構(gòu)军浆,所以大多數(shù)指令都只有一個操作碼棕洋。比如 aload_0
(將局部變量表中下標為 0 的數(shù)據(jù)壓入操作數(shù)棧中)就只有操作碼沒有操作數(shù),而 invokespecial #1
(調(diào)用成員方法或者構(gòu)造方法乒融,并傳遞常量池中下標為 1 的常量)就是由操作碼和操作數(shù)組成的掰盘。
01摄悯、加載與存儲指令
加載(load)和存儲(store)相關(guān)的指令是使用最頻繁的指令,用于將數(shù)據(jù)從棧幀的局部變量表和操作數(shù)棧之間來回傳遞愧捕。
1)將局部變量表中的變量壓入操作數(shù)棧中
- xload_<n>(x 為 i奢驯、l、f次绘、d瘪阁、a,n 默認為 0 到 3)邮偎,表示將第 n 個局部變量壓入操作數(shù)棧中管跺。
- xload(x 為 i、l钢猛、f伙菜、d、a)命迈,通過指定參數(shù)的形式贩绕,將局部變量壓入操作數(shù)棧中,當使用這個指令時壶愤,表示局部變量的數(shù)量可能超過了 4 個
解釋一下淑倾。
x 為操作碼助記符,表明是哪一種數(shù)據(jù)類型征椒。見下表所示娇哆。
像 arraylength 指令,沒有操作碼助記符勃救,它沒有代表數(shù)據(jù)類型的特殊字符碍讨,但操作數(shù)只能是一個數(shù)組類型的對象。
大部分的指令都不支持 byte蒙秒、short 和 char勃黍,甚至沒有任何指令支持 boolean 類型。編譯器會將 byte 和 short 類型的數(shù)據(jù)帶符號擴展(Sign-Extend)為 int 類型晕讲,將 boolean 和 char 零位擴展(Zero-Extend)為 int 類型覆获。
舉例來說。
private void load(int age, String name, long birthday, boolean sex) {
System.out.println(age + name + birthday + sex);
}
通過 jclasslib 看一下 load()
方法(4 個參數(shù))的字節(jié)碼指令瓢省。
- iload_1:將局部變量表中下標為 1 的 int 變量壓入操作數(shù)棧中羔飞。
- aload_2:將局部變量表中下標為 2 的引用數(shù)據(jù)類型變量(此時為 String)壓入操作數(shù)棧中驯鳖。
- lload_3:將局部變量表中下標為 3 的 long 型變量壓入操作數(shù)棧中。
- iload 5:將局部變量表中下標為 5 的 int 變量(實際為 boolean)壓入操作數(shù)棧中。
通過查看局部變量表就能關(guān)聯(lián)上了硬爆。
2)將常量池中的常量壓入操作數(shù)棧中
根據(jù)數(shù)據(jù)類型和入棧內(nèi)容的不同潮改,此類又可以細分為 const 系列、push 系列和 Idc 指令。
const 系列废亭,用于特殊的常量入棧,要入棧的常量隱含在指令本身具钥。
push 系列豆村,主要包括 bipush 和 sipush,前者接收 8 位整數(shù)作為參數(shù)骂删,后者接收 16 位整數(shù)掌动。
Idc 指令,當 const 和 push 不能滿足的時候宁玫,萬能的 Idc 指令就上場了粗恢,它接收一個 8 位的參數(shù),指向常量池中的索引欧瘪。
-
Idc_w
:接收兩個 8 位數(shù)眷射,索引范圍更大。 - 如果參數(shù)是 long 或者 double佛掖,使用
Idc2_w
指令妖碉。
舉例來說。
public void pushConstLdc() {
// 范圍 [-1,5]
int iconst = -1;
// 范圍 [-128,127]
int bipush = 127;
// 范圍 [-32768,32767]
int sipush= 32767;
// 其他 int
int ldc = 32768;
String aconst = null;
String IdcString = "沉默王二";
}
通過 jclasslib 看一下 pushConstLdc()
方法的字節(jié)碼指令芥被。
- iconst_m1:將 -1 入棧欧宜。范圍 [-1,5]。
- bipush 127:將 127 入棧拴魄。范圍 [-128,127]冗茸。
- sipush 32767:將 32767 入棧。范圍 [-32768,32767]匹中。
- ldc #6 <32768>:將常量池中下標為 6 的常量 32768 入棧夏漱。
- aconst_null:將 null 入棧。
- ldc #7 <沉默王二>:將常量池中下標為 7 的常量“沉默王二”入棧顶捷。
3)將棧頂?shù)臄?shù)據(jù)出棧并裝入局部變量表中
主要是用來給局部變量賦值挂绰,這類指令主要以 store 的形式存在。
- xstore_<n>(x 為 i焊切、l扮授、f芳室、d专肪、a,n 默認為 0 到 3)
- xstore(x 為 i堪侯、l嚎尤、f、d伍宦、a)
明白了 xload_<n> 和 xload芽死,再看 xstore_<n> 和 xstore 就會輕松得多乏梁,作用反了一下而已。
大家來想一個問題关贵,為什么要有 xstore_<n> 和 xload_<n> 呢遇骑?它們的作用和 xstore n、xload n 不是一樣的嗎揖曾?
xstore_<n> 和 xstore n 的區(qū)別在于落萎,前者相當于只有操作碼,占用 1 個字節(jié)炭剪;后者相當于由操作碼和操作數(shù)組成练链,操作碼占 1 個字節(jié),操作數(shù)占 2 個字節(jié)奴拦,一共占 3 個字節(jié)媒鼓。
由于局部變量表中前幾個位置總是非常常用,雖然 xstore_<n>
和 xload_<n>
增加了指令數(shù)量错妖,但字節(jié)碼的體積變小了绿鸣!
舉例來說。
public void store(int age, String name) {
int temp = age + 2;
String str = name;
}
通過 jclasslib 看一下 store()
方法的字節(jié)碼指令暂氯。
- istore_3:從操作數(shù)中彈出一個整數(shù)枚驻,并把它賦值給局部變量表中索引為 3 的變量。
- astore 4:從操作數(shù)中彈出一個引用數(shù)據(jù)類型株旷,并把它賦值給局部變量表中索引為 4 的變量再登。
通過查看局部變量表就能關(guān)聯(lián)上了。
02晾剖、算術(shù)指令
算術(shù)指令用于對兩個操作數(shù)棧上的值進行某種特定運算锉矢,并把結(jié)果重新壓入操作數(shù)棧〕菥。可以分為兩類:整型數(shù)據(jù)的運算指令和浮點數(shù)據(jù)的運算指令沽损。
需要注意的是,數(shù)據(jù)運算可能會導致溢出循头,比如兩個很大的正整數(shù)相加绵估,很可能會得到一個負數(shù)。但 Java 虛擬機規(guī)范中并沒有對這種情況給出具體結(jié)果卡骂,因此程序是不會顯式報錯的国裳。所以,大家在開發(fā)過程中全跨,如果涉及到較大的數(shù)據(jù)進行加法缝左、乘法運算的時候,一定要注意!
當發(fā)生溢出時渺杉,將會使用有符號的無窮大 Infinity 來表示蛇数;如果某個操作結(jié)果沒有明確的數(shù)學定義的話,將會使用 NaN 值來表示是越。而且所有使用 NaN 作為操作數(shù)的算術(shù)操作耳舅,結(jié)果都會返回 NaN。
舉例來說倚评。
public void infinityNaN() {
int i = 10;
double j = i / 0.0;
System.out.println(j); // Infinity
double d1 = 0.0;
double d2 = d1 / 0.0;
System.out.println(d2); // NaN
}
- 任何一個非零的數(shù)除以浮點數(shù) 0(注意不是 int 類型)挽放,可以想象結(jié)果是無窮大 Infinity 的。
- 把這個非零的數(shù)換成 0 的時候蔓纠,結(jié)果又不太好定義辑畦,就用 NaN 值來表示。
Java 虛擬機提供了兩種運算模式:
- 向最接近數(shù)舍入:在進行浮點數(shù)運算時腿倚,所有的結(jié)果都必須舍入到一個適當?shù)木却砍觯皇翘貏e精確的結(jié)果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值接近敷燎,將優(yōu)先選擇最低有效位為零的(類似四舍五入)暂筝。
- 向零舍入:將浮點數(shù)轉(zhuǎn)換為整數(shù)時,采用該模式硬贯,該模式將在目標數(shù)值類型中選擇一個最接近但是不大于原值的數(shù)字作為最精確的舍入結(jié)果(類似取整)焕襟。
我把所有的算術(shù)指令列一下:
- 加法指令:iadd、ladd饭豹、fadd鸵赖、dadd
- 減法指令:isub、lsub拄衰、fsub它褪、dsub
- 乘法指令:imul、lmul翘悉、fmul茫打、dmul
- 除法指令:idiv、ldiv妖混、fdiv老赤、ddiv
- 求余指令:irem、lrem制市、frem抬旺、drem
- 自增指令:iinc
舉例來說。
public void calculate(int age) {
int add = age + 1;
int sub = age - 1;
int mul = age * 2;
int div = age / 3;
int rem = age % 4;
age++;
age--;
}
通過 jclasslib 看一下 calculate()
方法的字節(jié)碼指令息堂。
- iadd嚷狞,加法
- isub,減法
- imul荣堰,乘法
- idiv床未,除法
- irem,取余
- iinc振坚,自增的時候 +1薇搁,自減的時候 -1
03、類型轉(zhuǎn)換指令
可以分為兩種:
1)寬化渡八,小類型向大類型轉(zhuǎn)換啃洋,比如 int–>long–>float–>double
,對應(yīng)的指令有:i2l屎鳍、i2f宏娄、i2d、l2f逮壁、l2d孵坚、f2d。
- 從 int 到 long窥淆,或者從 int 到 double卖宠,是不會有精度丟失的;
- 從 int忧饭、long 到 float扛伍,或者 long 到 double 時,可能會發(fā)生精度丟失词裤;
- 從 byte刺洒、char 和 short 到 int 的寬化類型轉(zhuǎn)換實際上是隱式發(fā)生的,這樣可以減少字節(jié)碼指令吼砂,畢竟字節(jié)碼指令只有 256 個作媚,占一個字節(jié)。
2)窄化帅刊,大類型向小類型轉(zhuǎn)換纸泡,比如從 int 類型到 byte、short 或者 char赖瞒,對應(yīng)的指令有:i2b女揭、i2s、i2c栏饮;從 long 到 int吧兔,對應(yīng)的指令有:l2i;從 float 到 int 或者 long袍嬉,對應(yīng)的指令有:f2i境蔼、f2l灶平;從 double 到 int、long 或者 float箍土,對應(yīng)的指令有:d2i逢享、d2l、d2f吴藻。
- 窄化很可能會發(fā)生精度丟失瞒爬,畢竟是不同的數(shù)量級;
- 但 Java 虛擬機并不會因此拋出運行時異常沟堡。
舉例來說侧但。
public void updown() {
int i = 10;
double d = i;
float f = 10f;
long ong = (long)f;
}
通過 jclasslib 看一下 updown()
方法的字節(jié)碼指令。
- i2d航罗,int 寬化為 double
- f2l禀横, float 窄化為 long
04、對象的創(chuàng)建和訪問指令
Java 是一門面向?qū)ο蟮木幊陶Z言粥血,那么 Java 虛擬機是如何從字節(jié)碼層面進行支持的呢燕侠?
1)創(chuàng)建指令
數(shù)組也是一種對象,但它創(chuàng)建的字節(jié)碼指令和普通的對象不同立莉。創(chuàng)建數(shù)組的指令有三種:
- newarray:創(chuàng)建基本數(shù)據(jù)類型的數(shù)組
- anewarray:創(chuàng)建引用類型的數(shù)組
- multianewarray:創(chuàng)建多維數(shù)組
普通對象的創(chuàng)建指令只有一個绢彤,就是 new
,它會接收一個操作數(shù)蜓耻,指向常量池中的一個索引茫舶,表示要創(chuàng)建的類型。
舉例來說刹淌。
public void newObject() {
String name = new String("沉默王二");
File file = new File("無愁河的浪蕩漢子.book");
int [] ages = {};
}
通過 jclasslib 看一下 newObject()
方法的字節(jié)碼指令饶氏。
-
new #13 <java/lang/String>
,創(chuàng)建一個 String 對象有勾。 -
new #15 <java/io/File>
疹启,創(chuàng)建一個 File 對象。 -
newarray 10 (int)
蔼卡,創(chuàng)建一個 int 類型的數(shù)組喊崖。
2)字段訪問指令
字段可以分為兩類,一類是成員變量雇逞,一類是靜態(tài)變量(static 關(guān)鍵字修飾的)荤懂,所以字段訪問指令可以分為兩類:
- 訪問靜態(tài)變量:getstatic、putstatic塘砸。
- 訪問成員變量:getfield节仿、putfield,需要創(chuàng)建對象后才能訪問掉蔬。
舉例來說廊宪。
public class Writer {
private String name;
static String mark = "作者";
public static void main(String[] args) {
print(mark);
Writer w = new Writer();
print(w.name);
}
public static void print(String arg) {
System.out.println(arg);
}
}
通過 jclasslib 看一下 main()
方法的字節(jié)碼指令矾瘾。
-
getstatic #2 <com/itwanger/jvm/Writer.mark>
,訪問靜態(tài)變量 mark -
getfield #6 <com/itwanger/jvm/Writer.name>
箭启,訪問成員變量 name
05壕翩、方法調(diào)用和返回指令
方法調(diào)用指令有 5 個,分別用于不同的場景:
- invokevirtual:用于調(diào)用對象的成員方法册烈,根據(jù)對象的實際類型進行分派戈泼,支持多態(tài)婿禽。
- invokeinterface:用于調(diào)用接口方法赏僧,會在運行時搜索由特定對象實現(xiàn)的接口方法進行調(diào)用。
- invokespecial:用于調(diào)用一些需要特殊處理的方法扭倾,包括構(gòu)造方法淀零、私有方法和父類方法。
- invokestatic:用于調(diào)用靜態(tài)方法膛壹。
- invokedynamic:用于在運行時動態(tài)解析出調(diào)用點限定符所引用的方法驾中,并執(zhí)行。
舉例來說模聋。
public class InvokeExamples {
private void run() {
List ls = new ArrayList();
ls.add("難頂");
ArrayList als = new ArrayList();
als.add("學不動了");
}
public static void print() {
System.out.println("invokestatic");
}
public static void main(String[] args) {
print();
InvokeExamples invoke = new InvokeExamples();
invoke.run();
}
}
我們用 javap -c InvokeExamples.class
來反編譯一下肩民。
Compiled from "InvokeExamples.java"
public class com.itwanger.jvm.InvokeExamples {
public com.itwanger.jvm.InvokeExamples();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
private void run();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String 難頂
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: new #2 // class java/util/ArrayList
20: dup
21: invokespecial #3 // Method java/util/ArrayList."<init>":()V
24: astore_2
25: aload_2
26: ldc #6 // String 學不動了
28: invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
31: pop
32: return
public static void print();
Code:
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #9 // String invokestatic
5: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #11 // Method print:()V
3: new #12 // class com/itwanger/jvm/InvokeExamples
6: dup
7: invokespecial #13 // Method "<init>":()V
10: astore_1
11: aload_1
12: invokevirtual #14 // Method run:()V
15: return
}
InvokeExamples 類有 4 個方法,包括缺省的構(gòu)造方法在內(nèi)链方。
1)InvokeExamples()
構(gòu)造方法中
缺省的構(gòu)造方法內(nèi)部會調(diào)用超類 Object 的初始化構(gòu)造方法:
`invokespecial #1 // Method java/lang/Object."<init>":()V`
2)成員方法 run()
中
invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
由于 ls 變量的引用類型為接口 List持痰,所以 ls.add()
調(diào)用的是 invokeinterface
指令,等運行時再確定是不是接口 List 的實現(xiàn)對象 ArrayList 的 add()
方法祟蚀。
invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
由于 als 變量的引用類型已經(jīng)確定為 ArrayList工窍,所以 als.add()
方法調(diào)用的是 invokevirtual
指令。
3)main()
方法中
invokestatic #11 // Method print:()V
print()
方法是靜態(tài)的前酿,所以調(diào)用的是 invokestatic
指令患雏。
方法返回指令根據(jù)方法的返回值類型進行區(qū)分,常見的返回指令見下圖罢维。
06淹仑、操作數(shù)棧管理指令
常見的操作數(shù)棧管理指令有 pop、dup 和 swap肺孵。
- 將一個或兩個元素從棧頂彈出攻人,并且直接廢棄,比如 pop悬槽,pop2怀吻;
- 復制棧頂?shù)囊粋€或兩個數(shù)值并將其重新壓入棧頂,比如 dup初婆,dup2蓬坡,dup_×1猿棉,dup2_×1,dup_×2屑咳,dup2_×2萨赁;
- 將棧最頂端的兩個槽中的數(shù)值交換位置,比如 swap兆龙。
這些指令不需要指明數(shù)據(jù)類型杖爽,因為是按照位置壓入和彈出的。
舉例來說紫皇。
public class Dup {
int age;
public int incAndGet() {
return ++age;
}
}
通過 jclasslib 看一下 incAndGet()
方法的字節(jié)碼指令慰安。
- aload_0:將 this 入棧。
- dup:復制棧頂?shù)?this聪铺。
- getfield #2:將常量池中下標為 2 的常量加載到棧上化焕,同時將一個 this 出棧。
- iconst_1:將常量 1 入棧铃剔。
- iadd:將棧頂?shù)膬蓚€值相加后出棧撒桨,并將結(jié)果放回棧上。
- dup_x1:復制棧頂?shù)脑丶担⑵洳迦?this 下面凤类。
- putfield #2: 將棧頂?shù)膬蓚€元素出棧,并將其賦值給字段 age普气。
- ireturn:將棧頂?shù)脑爻鰲7祷亍?/li>
07谜疤、控制轉(zhuǎn)移指令
控制轉(zhuǎn)移指令包括:
- 比較指令,比較棧頂?shù)膬蓚€元素的大小棋电,并將比較結(jié)果入棧茎截。
- 條件跳轉(zhuǎn)指令,通常和比較指令一塊使用赶盔,在條件跳轉(zhuǎn)指令執(zhí)行前企锌,一般先用比較指令進行棧頂元素的比較,然后進行條件跳轉(zhuǎn)于未。
- 比較條件轉(zhuǎn)指令撕攒,類似于比較指令和條件跳轉(zhuǎn)指令的結(jié)合體,它將比較和跳轉(zhuǎn)兩個步驟合二為一烘浦。
- 多條件分支跳轉(zhuǎn)指令抖坪,專為 switch-case 語句設(shè)計的。
- 無條件跳轉(zhuǎn)指令闷叉,目前主要是 goto 指令擦俐。
1)比較指令
比較指令有:dcmpg,dcmpl、fcmpg榨为、fcmpl、lcmp罕拂,指令的第一個字母代表的含義分別是 double埋合、float备徐、long。注意甚颂,沒有 int 類型蜜猾。
對于 double 和 float 來說,由于 NaN 的存在振诬,有兩個版本的比較指令蹭睡。拿 float 來說,有 fcmpg 和 fcmpl贷揽,區(qū)別在于棠笑,如果遇到 NaN梦碗,fcmpg 會將 1 壓入棧禽绪,fcmpl 會將 -1 壓入棧。
舉例來說洪规。
public void lcmp(long a, long b) {
if(a > b){}
}
通過 jclasslib 看一下 lcmp()
方法的字節(jié)碼指令印屁。
lcmp 用于兩個 long 型的數(shù)據(jù)進行比較。
2)條件跳轉(zhuǎn)指令
這些指令都會接收兩個字節(jié)的操作數(shù)斩例,它們的統(tǒng)一含義是雄人,彈出棧頂元素,測試它是否滿足某一條件念赶,滿足的話础钠,跳轉(zhuǎn)到對應(yīng)位置。
對于 long叉谜、float 和 double 類型的條件分支比較旗吁,會先執(zhí)行比較指令返回一個整形值到操作數(shù)棧中后再執(zhí)行 int 類型的條件跳轉(zhuǎn)指令。
對于 boolean停局、byte很钓、char、short董栽,以及 int码倦,則直接使用條件跳轉(zhuǎn)指令來完成。
舉例來說锭碳。
public void fi() {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
通過 jclasslib 看一下 fi()
方法的字節(jié)碼指令袁稽。
3 ifne 12 (+9)
的意思是,如果棧頂?shù)脑夭坏扔?0擒抛,跳轉(zhuǎn)到第 12(3+9)行 12 bipush 20
推汽。
3)比較條件轉(zhuǎn)指令
前綴“if_”后蝗柔,以字符“i”開頭的指令針對 int 型整數(shù)進行操作,以字符“a”開頭的指令表示對象的比較民泵。
舉例來說癣丧。
public void compare() {
int i = 10;
int j = 20;
System.out.println(i > j);
}
通過 jclasslib 看一下 compare()
方法的字節(jié)碼指令。
11 if_icmple 18 (+7)
的意思是栈妆,如果棧頂?shù)膬蓚€ int 類型的數(shù)值比較的話胁编,如果前者小于后者時跳轉(zhuǎn)到第 18 行(11+7)。
4)多條件分支跳轉(zhuǎn)指令
主要有 tableswitch 和 lookupswitch鳞尔,前者要求多個條件分支值是連續(xù)的嬉橙,它內(nèi)部只存放起始值和終止值,以及若干個跳轉(zhuǎn)偏移量寥假,通過給定的操作數(shù) index市框,可以立即定位到跳轉(zhuǎn)偏移量位置,因此效率比較高糕韧;后者內(nèi)部存放著各個離散的 case-offset 對枫振,每次執(zhí)行都要搜索全部的 case-offset 對,找到匹配的 case 值萤彩,并根據(jù)對應(yīng)的 offset 計算跳轉(zhuǎn)地址粪滤,因此效率較低。
舉例來說雀扶。
public void switchTest(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
case 3:
num = 30;
break;
default:
num = 40;
}
}
通過 jclasslib 看一下 switchTest()
方法的字節(jié)碼指令杖小。
case 2 的時候沒有 break,所以 case 2 和 case 3 是連續(xù)的愚墓,用的是 tableswitch予权。如果等于 1,跳轉(zhuǎn)到 28 行浪册;如果等于 2 和 3扫腺,跳轉(zhuǎn)到 34 行,如果是 default议经,跳轉(zhuǎn)到 40 行斧账。
5)無條件跳轉(zhuǎn)指令
goto 指令接收兩個字節(jié)的操作數(shù),共同組成一個帶符號的整數(shù)煞肾,用于指定指令的偏移量咧织,指令執(zhí)行的目的就是跳轉(zhuǎn)到偏移量給定的位置處。
前面的例子里都出現(xiàn)了 goto 的身影籍救,也很好理解习绢。如果指令的偏移量特別大,超出了兩個字節(jié)的范圍,可以使用指令 goto_w闪萄,接收 4 個字節(jié)的操作數(shù)梧却。
巨人的肩膀:
除了以上這些指令,還有異常處理指令和同步控制指令败去,我打算吊一吊大家的胃口放航,大家可以期待一波~~
(騷操作)
路漫漫其修遠兮,吾將上下而求索
想要走得更遠圆裕,Java 字節(jié)碼這塊就必須得硬碰硬地吃透广鳍,希望二哥的這些分享可以幫助到大家~
叨逼叨
二哥在 簡書 上寫了很多 Java 方面的系列文章,有 Java 核心語法吓妆、Java 集合框架赊时、Java IO、Java 并發(fā)編程行拢、Java 虛擬機等祖秒,也算是體系完整了。
為了能幫助到更多的 Java 初學者舟奠,二哥把自己連載的《教妹學Java》開源到了 GitHub竭缝,盡管只整理了 50 篇,發(fā)現(xiàn)字數(shù)已經(jīng)來到了 10 萬+鸭栖,內(nèi)容更是沒得說歌馍,通俗易懂握巢、風趣幽默晕鹊、圖文并茂。
GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java
如果有幫助的話暴浦,還請給二哥點個贊溅话,這將是我繼續(xù)分享下去的最強動力!