天天看點

我要悄悄學習 Java 位元組碼指令,在成為技術大佬的路上一去不複返(1)

大家好,我是二哥呀。

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 為操作碼助記符,表明是哪一種資料類型。見下表所示。

我要悄悄學習 Java 位元組碼指令,在成為技術大佬的路上一去不複返(1)

像 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 個參數)的位元組碼指令。

我要悄悄學習 Java 位元組碼指令,在成為技術大佬的路上一去不複返(1)

iload_1:将局部變量表中下标為 1 的 int 變量壓入操作數棧中。

aload_2:将局部變量表中下标為 2 的引用資料類型變量(此時為 String)壓入操作數棧中。

lload_3:将局部變量表中下标為 3 的 long 型變量壓入操作數棧中。

iload 5:将局部變量表中下标為 5 的 int 變量(實際為 boolean)壓入操作數棧中。

通過檢視局部變量表就能關聯上了。

我要悄悄學習 Java 位元組碼指令,在成為技術大佬的路上一去不複返(1)

2)将常量池中的常量壓入操作數棧中

根據資料類型和入棧内容的不同,此類又可以細分為 const 系列、push 系列和 Idc 指令。

const 系列,用于特殊的常量入棧,要入棧的常量隐含在指令本身。

我要悄悄學習 Java 位元組碼指令,在成為技術大佬的路上一去不複返(1)

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() 方法的位元組碼指令。

我要悄悄學習 Java 位元組碼指令,在成為技術大佬的路上一去不複返(1)

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> 增加了指令數量,但位元組碼的體積變小了!