大家好,我是二哥呀,
Java 位元組碼指令是 JVM 體系中非常難啃的一塊硬骨頭,我估計有些讀者會有這樣的疑惑,“Java 位元組碼難學嗎?我能不能學會啊?”
講良心話,不是我謙虛,一開始學 Java 位元組碼和 Java 虛擬機方面的知識我也感覺頭大!但硬著頭皮學了一陣子之后,突然就開竅了,覺得好有意思,尤其是明白了 Java 代碼在底層竟然是這樣執行的時候,感覺既膨脹又飄飄然,渾身上下散發著自信的光芒!
我在 CSDN 共輸出了 100 多篇 Java 方面的文章,總字數超過 30 萬字, 內容風趣幽默、通俗易懂,識訓了很多初學者的認可和支持,內容包括 Java 語法、Java 集合框架、Java 并發編程、Java 虛擬機等核心內容,

為了幫助更多的 Java 初學者,我“一怒之下”就把這些文章重新整理并開源到了 GitHub,起名《教妹學 Java》,聽起來是不是就很有趣?
GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java
Java 官方的虛擬機 Hotspot 是基于堆疊的,而不是基于暫存器的,
基于堆疊的優點是可移植性更好、指令更短、實作起來簡單,但不能隨機訪問堆疊中的元素,完成相同功能所需要的指令數也比暫存器的要多,需要頻繁的入堆疊和出堆疊,
基于暫存器的優點是速度快,有利于程式運行速度的優化,但運算元需要顯式指定,指令也比較長,
Java 位元組碼由操作碼和操作陣列成,
- 操作碼(Opcode):一個位元組長度(0-255,意味著指令集的操作碼總數不可能超過 256 條),代表著某種特定的操作含義,
- 運算元(Operands):零個或者多個,緊跟在操作碼之后,代表此操作需要的引數,
由于 Java 虛擬機是基于堆疊而不是暫存器的結構,所以大多數指令都只有一個操作碼,比如 aload_0(將區域變數表中下標為 0 的資料壓入運算元堆疊中)就只有操作碼沒有運算元,而 invokespecial #1(呼叫成員方法或者構造方法,并傳遞常量池中下標為 1 的常量)就是由操作碼和操作陣列成的,
01、加載與存盤指令
加載(load)和存盤(store)相關的指令是使用最頻繁的指令,用于將資料從堆疊幀的區域變數表和運算元堆疊之間來回傳遞,
1)將區域變數表中的變數壓入運算元堆疊中
- xload_(x 為 i、l、f、d、a,n 默認為 0 到 3),表示將第 n 個區域變數壓入運算元堆疊中,
- xload(x 為 i、l、f、d、a),通過指定引數的形式,將區域變數壓入運算元堆疊中,當使用這個指令時,表示區域變數的數量可能超過了 4 個
解釋一下,
x 為操作碼助記符,表明是哪一種資料型別,見下表所示,

像 arraylength 指令,沒有操作碼助記符,它沒有代表資料型別的特殊字符,但運算元只能是一個陣列型別的物件,
大部分的指令都不支持 byte、short 和 char,甚至沒有任何指令支持 boolean 型別,編譯器會將 byte 和 short 型別的資料帶符號擴展(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 個引數)的位元組碼指令,

- iload_1:將區域變數表中下標為 1 的 int 變數壓入運算元堆疊中,
- aload_2:將區域變數表中下標為 2 的參考資料型別變數(此時為 String)壓入運算元堆疊中,
- lload_3:將區域變數表中下標為 3 的 long 型變數壓入運算元堆疊中,
- iload 5:將區域變數表中下標為 5 的 int 變數(實際為 boolean)壓入運算元堆疊中,
通過查看區域變數表就能關聯上了,

2)將常量池中的常量壓入運算元堆疊中
根據資料型別和入堆疊內容的不同,此類又可以細分為 const 系列、push 系列和 Idc 指令,
const 系列,用于特殊的常量入堆疊,要入堆疊的常量隱含在指令本身,

push 系列,主要包括 bipush 和 sipush,前者接收 8 位整數作為引數,后者接收 16 位整數,
Idc 指令,當 const 和 push 不能滿足的時候,萬能的 Idc 指令就上場了,它接收一個 8 位的引數,指向常量池中的索引,
Idc_w:接收兩個 8 位數,索引范圍更大,- 如果引數是 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() 方法的位元組碼指令,

- 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)將堆疊頂的資料出堆疊并裝入區域變數表中
主要是用來給區域變數賦值,這類指令主要以 store 的形式存在,
- xstore_(x 為 i、l、f、d、a,n 默認為 0 到 3)
- xstore(x 為 i、l、f、d、a)
明白了 xload_ 和 xload,再看 xstore_ 和 xstore 就會輕松得多,作用反了一下而已,
大家來想一個問題,為什么要有 xstore_ 和 xload_ 呢?它們的作用和 xstore n、xload n 不是一樣的嗎?
xstore_ 和 xstore n 的區別在于,前者相當于只有操作碼,占用 1 個位元組;后者相當于由操作碼和操作陣列成,操作碼占 1 個位元組,運算元占 2 個位元組,一共占 3 個位元組,
由于區域變數表中前幾個位置總是非常常用,雖然 xstore_<n> 和 xload_<n> 增加了指令數量,但位元組碼的體積變小了!
舉例來說,
public void store(int age, String name) {
int temp = age + 2;
String str = name;
}
通過 jclasslib 看一下 store() 方法的位元組碼指令,

- istore_3:從運算元中彈出一個整數,并把它賦值給區域變數表中索引為 3 的變數,
- astore 4:從運算元中彈出一個參考資料型別,并把它賦值給區域變數表中索引為 4 的變數,
通過查看區域變數表就能關聯上了,

02、算術指令
算術指令用于對兩個運算元堆疊上的值進行某種特定運算,并把結果重新壓入運算元堆疊,可以分為兩類:整型資料的運算指令和浮點資料的運算指令,
需要注意的是,資料運算可能會導致溢位,比如兩個很大的正整數相加,很可能會得到一個負數,但 Java 虛擬機規范中并沒有對這種情況給出具體結果,因此程式是不會顯式報錯的,所以,大家在開發程序中,如果涉及到較大的資料進行加法、乘法運算的時候,一定要注意!
當發生溢位時,將會使用有符號的無窮大 Infinity 來表示;如果某個操作結果沒有明確的數學定義的話,將會使用 NaN 值來表示,而且所有使用 NaN 作為運算元的算術操作,結果都會回傳 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
}
- 任何一個非零的數除以浮點數 0(注意不是 int 型別),可以想象結果是無窮大 Infinity 的,
- 把這個非零的數換成 0 的時候,結果又不太好定義,就用 NaN 值來表示,
Java 虛擬機提供了兩種運算模式:
- 向最接近數舍入:在進行浮點數運算時,所有的結果都必須舍入到一個適當的精度,不是特別精確的結果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值接近,將優先選擇最低有效位為零的(類似四舍五入),
- 向零舍入:將浮點數轉換為整數時,采用該模式,該模式將在目標數值型別中選擇一個最接近但是不大于原值的數字作為最精確的舍入結果(類似取整),
我把所有的算術指令列一下:
- 加法指令: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() 方法的位元組碼指令,

- iadd,加法
- isub,減法
- imul,乘法
- idiv,除法
- irem,取余
- iinc,自增的時候 +1,自減的時候 -1
03、型別轉換指令
可以分為兩種:
1)寬化,小型別向大型別轉換,比如 int–>long–>float–>double,對應的指令有:i2l、i2f、i2d、l2f、l2d、f2d,
- 從 int 到 long,或者從 int 到 double,是不會有精度丟失的;
- 從 int、long 到 float,或者 long 到 double 時,可能會發生精度丟失;
- 從 byte、char 和 short 到 int 的寬化型別轉換實際上是隱式發生的,這樣可以減少位元組碼指令,畢竟位元組碼指令只有 256 個,占一個位元組,
2)窄化,大型別向小型別轉換,比如從 int 型別到 byte、short 或者 char,對應的指令有:i2b、i2s、i2c;從 long 到 int,對應的指令有:l2i;從 float 到 int 或者 long,對應的指令有:f2i、f2l;從 double 到 int、long 或者 float,對應的指令有:d2i、d2l、d2f,
- 窄化很可能會發生精度丟失,畢竟是不同的數量級;
- 但 Java 虛擬機并不會因此拋出運行時例外,
舉例來說,
public void updown() {
int i = 10;
double d = i;
float f = 10f;
long ong = (long)f;
}
通過 jclasslib 看一下 updown() 方法的位元組碼指令,

- i2d,int 寬化為 double
- f2l, float 窄化為 long
04、物件的創建和訪問指令
Java 是一門面向物件的編程語言,那么 Java 虛擬機是如何從位元組碼層面進行支持的呢?
1)創建指令
陣列也是一種物件,但它創建的位元組碼指令和普通的物件不同,創建陣列的指令有三種:
- newarray:創建基本資料型別的陣列
- anewarray:創建參考型別的陣列
- multianewarray:創建多維陣列
普通物件的創建指令只有一個,就是 new,它會接收一個運算元,指向常量池中的一個索引,表示要創建的型別,
舉例來說,
public void newObject() {
String name = new String("沉默王二");
File file = new File("無愁河的浪蕩漢子.book");
int [] ages = {};
}
通過 jclasslib 看一下 newObject() 方法的位元組碼指令,

new #13 <java/lang/String>,創建一個 String 物件,new #15 <java/io/File>,創建一個 File 物件,newarray 10 (int),創建一個 int 型別的陣列,
2)欄位訪問指令
欄位可以分為兩類,一類是成員變數,一類是靜態變數(static 關鍵字修飾的),所以欄位訪問指令可以分為兩類:
- 訪問靜態變數:getstatic、putstatic,
- 訪問成員變數:getfield、putfield,需要創建物件后才能訪問,
舉例來說,
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() 方法的位元組碼指令,

getstatic #2 <com/itwanger/jvm/Writer.mark>,訪問靜態變數 markgetfield #6 <com/itwanger/jvm/Writer.name>,訪問成員變數 name
05、方法呼叫和回傳指令
方法呼叫指令有 5 個,分別用于不同的場景:
- invokevirtual:用于呼叫物件的成員方法,根據物件的實際型別進行分派,支持多型,
- invokeinterface:用于呼叫介面方法,會在運行時搜索由特定物件實作的介面方法進行呼叫,
- invokespecial:用于呼叫一些需要特殊處理的方法,包括構造方法、私有方法和父類方法,
- invokestatic:用于呼叫靜態方法,
- invokedynamic:用于在運行時動態決議出呼叫點限定符所參考的方法,并執行,
舉例來說,
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 個方法,包括預設的構造方法在內,
1)InvokeExamples() 構造方法中
預設的構造方法內部會呼叫超類 Object 的初始化構造方法:
`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() 呼叫的是 invokeinterface 指令,等運行時再確定是不是介面 List 的實作物件 ArrayList 的 add() 方法,
invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
由于 als 變數的參考型別已經確定為 ArrayList,所以 als.add() 方法呼叫的是 invokevirtual 指令,
3)main() 方法中
invokestatic #11 // Method print:()V
print() 方法是靜態的,所以呼叫的是 invokestatic 指令,
方法回傳指令根據方法的回傳值型別進行區分,常見的回傳指令見下圖,

06、運算元堆疊管理指令
常見的運算元堆疊管理指令有 pop、dup 和 swap,
- 將一個或兩個元素從堆疊頂彈出,并且直接廢棄,比如 pop,pop2;
- 復制堆疊頂的一個或兩個數值并將其重新壓入堆疊頂,比如 dup,dup2,dup_×1,dup2_×1,dup_×2,dup2_×2;
- 將堆疊最頂端的兩個槽中的數值交換位置,比如 swap,
這些指令不需要指明資料型別,因為是按照位置壓入和彈出的,
舉例來說,
public class Dup {
int age;
public int incAndGet() {
return ++age;
}
}
通過 jclasslib 看一下 incAndGet() 方法的位元組碼指令,

- aload_0:將 this 入堆疊,
- dup:復制堆疊頂的 this,
- getfield #2:將常量池中下標為 2 的常量加載到堆疊上,同時將一個 this 出堆疊,
- iconst_1:將常量 1 入堆疊,
- iadd:將堆疊頂的兩個值相加后出堆疊,并將結果放回堆疊上,
- dup_x1:復制堆疊頂的元素,并將其插入 this 下面,
- putfield #2: 將堆疊頂的兩個元素出堆疊,并將其賦值給欄位 age,
- ireturn:將堆疊頂的元素出堆疊回傳,
07、控制轉移指令
控制轉移指令包括:
- 比較指令,比較堆疊頂的兩個元素的大小,并將比較結果入堆疊,
- 條件跳轉指令,通常和比較指令一塊使用,在條件跳轉指令執行前,一般先用比較指令進行堆疊頂元素的比較,然后進行條件跳轉,
- 比較條件轉指令,類似于比較指令和條件跳轉指令的結合體,它將比較和跳轉兩個步驟合二為一,
- 多條件分支跳轉指令,專為 switch-case 陳述句設計的,
- 無條件跳轉指令,目前主要是 goto 指令,
1)比較指令
比較指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp,指令的第一個字母代表的含義分別是 double、float、long,注意,沒有 int 型別,
對于 double 和 float 來說,由于 NaN 的存在,有兩個版本的比較指令,拿 float 來說,有 fcmpg 和 fcmpl,區別在于,如果遇到 NaN,fcmpg 會將 1 壓入堆疊,fcmpl 會將 -1 壓入堆疊,
舉例來說,
public void lcmp(long a, long b) {
if(a > b){}
}
通過 jclasslib 看一下 lcmp() 方法的位元組碼指令,

lcmp 用于兩個 long 型的資料進行比較,
2)條件跳轉指令

這些指令都會接收兩個位元組的運算元,它們的統一含義是,彈出堆疊頂元素,測驗它是否滿足某一條件,滿足的話,跳轉到對應位置,
對于 long、float 和 double 型別的條件分支比較,會先執行比較指令回傳一個整形值到運算元堆疊中后再執行 int 型別的條件跳轉指令,
對于 boolean、byte、char、short,以及 int,則直接使用條件跳轉指令來完成,
舉例來說,
public void fi() {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
通過 jclasslib 看一下 fi() 方法的位元組碼指令,

3 ifne 12 (+9) 的意思是,如果堆疊頂的元素不等于 0,跳轉到第 12(3+9)行 12 bipush 20,
3)比較條件轉指令

前綴“if_”后,以字符“i”開頭的指令針對 int 型整數進行操作,以字符“a”開頭的指令表示物件的比較,
舉例來說,
public void compare() {
int i = 10;
int j = 20;
System.out.println(i > j);
}
通過 jclasslib 看一下 compare() 方法的位元組碼指令,

11 if_icmple 18 (+7) 的意思是,如果堆疊頂的兩個 int 型別的數值比較的話,如果前者小于后者時跳轉到第 18 行(11+7),
4)多條件分支跳轉指令
主要有 tableswitch 和 lookupswitch,前者要求多個條件分支值是連續的,它內部只存放起始值和終止值,以及若干個跳轉偏移量,通過給定的運算元 index,可以立即定位到跳轉偏移量位置,因此效率比較高;后者內部存放著各個離散的 case-offset 對,每次執行都要搜索全部的 case-offset 對,找到匹配的 case 值,并根據對應的 offset 計算跳轉地址,因此效率較低,
舉例來說,
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() 方法的位元組碼指令,

case 2 的時候沒有 break,所以 case 2 和 case 3 是連續的,用的是 tableswitch,如果等于 1,跳轉到 28 行;如果等于 2 和 3,跳轉到 34 行,如果是 default,跳轉到 40 行,
5)無條件跳轉指令
goto 指令接收兩個位元組的運算元,共同組成一個帶符號的整數,用于指定指令的偏移量,指令執行的目的就是跳轉到偏移量給定的位置處,
前面的例子里都出現了 goto 的身影,也很好理解,如果指令的偏移量特別大,超出了兩個位元組的范圍,可以使用指令 goto_w,接收 4 個位元組的運算元,
巨人的肩膀:
https://segmentfault.com/a/1190000037628881
除了以上這些指令,還有例外處理指令和同步控制指令,我打算吊一吊大家的胃口,大家可以期待一波~~
(騷操作)
路漫漫其修遠兮,吾將上下而求索
想要走得更遠,Java 位元組碼這塊就必須得硬碰硬地吃透,希望二哥的這些分享可以幫助到大家~
叨逼叨
二哥在 CSDN 上寫了很多 Java 方面的系列文章,有 Java 核心語法、Java 集合框架、Java IO、Java 并發編程、Java 虛擬機等,也算是體系完整了,

為了能幫助到更多的 Java 初學者,二哥把自己連載的《教妹學Java》開源到了 GitHub,盡管只整理了 50 篇,發現字數已經來到了 10 萬+,內容更是沒得說,通俗易懂、風趣幽默、圖文并茂,
GitHub 開源地址(歡迎 star):https://github.com/itwanger/jmx-java
如果有幫助的話,還請給二哥點個贊,這將是我繼續分享下去的最強動力!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/291635.html
標籤:java
