位元組碼指令

N1ce2cu發表於2024-07-16

載入與儲存指令


public int add(int a, int b) {
    int res = a + b;
    return res;
}
  • 位元組碼指令
public int add(int, int);
    descriptor: (II)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_3
         5: ireturn

1. 將區域性變數表中的變數壓入運算元棧

  • xload_<n>(x 為 i、l、f、d、a,n 預設為 0 到 3),表示將第 n 個區域性變數壓入運算元棧中。
  • xload(x 為 i、l、f、d、a),透過指定引數的形式,將區域性變數壓入運算元棧中,當使用這個指令時,表示區域性變數的數量可能超過了 4 個
操作碼助記符 資料型別
i int
l long
s short
b byte
c char
f float
d double
a 引用資料型別
  • 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);
}

load方法位元組碼:

 0 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
 3 new #3 <java/lang/StringBuilder>
 6 dup
 7 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
// 將區域性變數表中下標為 1 的 int 變數壓入運算元棧中
10 iload_1
11 invokevirtual #5 <java/lang/StringBuilder.append : (I)Ljava/lang/StringBuilder;>
// 將區域性變數表中下標為 2 的引用資料型別變數(此時為 String)壓入運算元棧中
14 aload_2
15 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
// 將區域性變數表中下標為 3 的 long 型變數壓入運算元棧中
18 lload_3
19 invokevirtual #7 <java/lang/StringBuilder.append : (J)Ljava/lang/StringBuilder;>
// 將區域性變數表中下標為 5 的 int 變數(實際為 boolean)壓入運算元棧中
22 iload 5
24 invokevirtual #8 <java/lang/StringBuilder.append : (Z)Ljava/lang/StringBuilder;>
27 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
30 invokevirtual #10 <java/io/PrintStream.println : (Ljava/lang/String;)V>
33 return

2.將常量池中的常量壓入運算元棧中

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

2.1 const系列

  • 用於特殊的常量入棧,要入棧的常量隱含在指令本身
指令 含義
iconst_<n> n從-1到5
lconst_<n> n從0到1
fconst_<n> n從0到2
dconst_<n> n從0到1
aconst_null 將null入棧

2.2 push系列

  • 主要包括 bipushsipush,前者接收 8 位整數作為引數,後者接收 16 位整數。

2.3 ldc系列

  • 它接收一個 8 位的引數,指向常量池中的索引。

  • ldc_w:接收兩個 8 位數,索引範圍更大。

  • 如果引數是 long 或者 double,使用 ldc2_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 ldcString = "hh";
}
// 將 -1 入棧 
 0 iconst_m1
 1 istore_1
// 將 127 入棧
 2 bipush 127
 4 istore_2
// 將 32767 入棧
 5 sipush 32767
 8 istore_3
// 將常量池中下標為 2 的常量 32768 入棧
 9 ldc #2 <32768>
11 istore 4
// 將 null 入棧
13 aconst_null
14 astore 5
// 將常量池中下標為 3 的常量“hh”入棧
16 ldc #3 <hh>
18 astore 6
20 return

3. 將棧頂的資料出棧並裝入區域性變數表中

  • xstore_<n>(x 為 i、l、f、d、a,n 預設為 0 到 3)
  • xstore(x 為 i、l、f、d、a)
public void fun(int age, String name) {
    int temp = age + 2;
    String str = name;
}

區域性變數表:

fun的位元組碼:

// 將區域性變數表中下標為 1 的 int 變數壓入運算元棧中
0 iload_1
// 將常量 2 入棧
1 iconst_2
2 iadd
// 從運算元中彈出一個整數,並把它賦值給區域性變數表中索引為 3 的變數
3 istore_3
// 將區域性變數表中下標為 2 的引用資料型別變數(此時為 String)壓入運算元棧中
4 aload_2
// 從運算元中彈出一個引用資料型別,並把它賦值給區域性變數表中索引為 4 的變數
5 astore 4
7 return

算術指令


  • 算術指令用於對兩個運算元棧上的值進行某種特定運算,並把結果重新壓入運算元棧。可以分為兩類:整型資料的運算指令和浮點資料的運算指令。

  • 資料運算可能會導致溢位,但 Java 虛擬機器規範中並沒有對這種情況給出具體結果,因此程式是不會顯式報錯;當發生溢位時,將會使用有符號的無窮大 Infinity 來表示;如果某個操作結果沒有明確的數學定義的話,將會使用 NaN 值來表示。而且所有使用 NaN 作為運算元的算術操作,結果都會返回 NaN。

  • Java 虛擬機器提供了兩種運算模式:

    • 向最接近數舍入:在進行浮點數運算時,所有的結果都必須舍入到一個適當的精度,不是特別精確的結果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值接近,將優先選擇最低有效位為零的(類似四捨五入)。

    • 向零舍入:將浮點數轉換為整數時,採用該模式,該模式將在目標數值型別中選擇一個最接近但是不大於原值的數字作為最精確的舍入結果(類似取整)。

  • 算術指令:

    • 加法指令:iadd、ladd、fadd、dadd

    • 減法指令:isub、lsub、fsub、dsub

    • 乘法指令:imul、lmul、fmul、dmul

    • 除法指令:idiv、ldiv、fdiv、ddiv

    • 求餘指令:irem、lrem、frem、drem

    • 自增指令:iinc

型別轉換指令


型別轉換指令可以分為兩種:

  • 寬化,小型別向大型別轉換,比如 int–>long–>float–>double,對應的指令有:i2l、i2f、i2d、l2f、l2d、f2d

    • 從 int 到 long,或者從 int 到 double,是不會有精度丟失的;

    • 從 int、long 到 float,或者 long 到 double 時,可能會發生精度丟失;

    • 從 byte、char 和 short 到 int 的寬化型別轉換實際上是隱式發生的,這樣可以減少位元組碼指令,畢竟位元組碼指令只有 256 個,佔一個位元組。

  • 窄化,大型別向小型別轉換,比如從 int 型別到 byte、short 或者 char,對應的指令有:i2b、i2s、i2c;從 long 到 int,對應的指令有:l2i;從 float 到 int 或者 long,對應的指令有:f2i、f2l;從 double 到 int、long 或者 float,對應的指令有:d2i、d2l、d2f

物件的建立和訪問指令


建立指令

  • 建立陣列的指令有三種:

    • newarray:建立基本資料型別的陣列

    • anewarray:建立引用型別的陣列

    • multianewarray:建立多維陣列

  • 建立物件指令只有一個,就是 new,它會接收一個運算元,指向常量池中的一個索引,表示要建立的型別。

public static void main(String[] args) {
    String name = new String("haha");
    File file = new File("xixi");
    int[] ages = {};
}
// 建立一個 String 物件
 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <haha>
 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1
// 建立一個 File 物件
10 new #5 <java/io/File>
13 dup
14 ldc #6 <xixi>
16 invokespecial #7 <java/io/File.<init> : (Ljava/lang/String;)V>
19 astore_2
20 iconst_0
// 建立一個 int 型別的陣列
21 newarray 10 (int)
23 astore_3
24 return

欄位訪問指令

  • 訪問靜態變數:getstaticputstatic
  • 訪問成員變數:getfieldputfield,需要建立物件後才能訪問。
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);
    }
}
// 訪問靜態變數 mark
 0 getstatic #2 <test/JVM/Writer.mark : Ljava/lang/String;>
 3 invokestatic #3 <test/JVM/Writer.print : (Ljava/lang/String;)V>
 6 new #4 <test/JVM/Writer>
 9 dup
10 invokespecial #5 <test/JVM/Writer.<init> : ()V>
13 astore_1
14 aload_1
// 訪問成員變數 name
15 getfield #6 <test/JVM/Writer.name : Ljava/lang/String;>
18 invokestatic #3 <test/JVM/Writer.print : (Ljava/lang/String;)V>
21 return

方法呼叫指令


  • invokevirtual:用於呼叫物件的成員方法,根據物件的實際型別進行分派,支援多型。
  • invokeinterface:用於呼叫介面方法,會在執行時搜尋由特定物件實現的介面方法進行呼叫。
  • invokespecial:用於呼叫一些需要特殊處理的方法,包括構造方法、私有方法和父類方法。
  • invokestatic:用於呼叫靜態方法。
  • invokedynamic:用於在執行時動態解析出呼叫點限定符所引用的方法,並執行。Lambda 表示式的實現就依賴於 invokedynamic 指令。
public static void main(String[] args) {
    // 使用 Lambda 表示式定義一個函式
    Function<Integer, Integer> square = x -> x * x;
    int result = square.apply(5);
    System.out.println(result);
}

// 使用 invokedynamic 呼叫一個引導方法(Bootstrap Method),這個方法負責實現並返回一個 Function 介面的例項
 0 invokedynamic #2 <apply, BootstrapMethods #0>
// astore_1:將 invokedynamic 指令的結果(Lambda 表示式的 Function 物件)儲存到區域性變數表的位置 1。
 5 astore_1
 6 aload_1
 7 iconst_5
 8 invokestatic #3 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
11 invokeinterface #4 <java/util/function/Function.apply : (Ljava/lang/Object;)Ljava/lang/Object;> count 2
16 checkcast #5 <java/lang/Integer>
19 invokevirtual #6 <java/lang/Integer.intValue : ()I>
22 istore_2
23 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
26 iload_2
27 invokevirtual #8 <java/io/PrintStream.println : (I)V>
30 return

lambda表示式的實現:lambda$main$0

返回指令


指令 型別
return void
ireturn int(boolean、byte、char、short)
lreturn long
freturn float
dreturn double
areturn 引用型別

運算元棧管理指令


常見的運算元棧管理指令有 pop、dup 和 swap

  • 將一個或兩個元素從棧頂彈出,並且直接廢棄,比如 pop,pop2;
  • 複製棧頂的一個或兩個數值並將其重新壓入棧頂,比如 dup,dup2,dup×1dup2_×1dup_×2dup2_×2
  • 將棧最頂端的兩個槽中的數值交換位置,比如 swap。

控制轉移指令


比較指令

  • 比較指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp,指令的第一個字母代表的含義分別是 double、float、long。注意,沒有 int 型別
  • 對於 double 和 float 來說,由於 NaN 的存在,有兩個版本的比較指令。拿 float 來說,有 fcmpg 和 fcmpl,區別在於,如果遇到 NaN,fcmpg 會將 1 壓入棧,fcmpl 會將 -1 壓入棧。

條件轉移指令

指令 含義
ifeq 等於0時跳轉
ifne 不等於0時跳轉
iflt 小於0時跳轉
ifle 小於等於0時跳轉
ifgt 大於0時跳轉
ifge 大於等於0時跳轉
ifnull 為null時跳轉
ifnonnull 不為null時跳轉
  • 這些指令都會接收兩個位元組的運算元,它們的統一含義是,彈出棧頂元素,測試它是否滿足某一條件,滿足的話,跳轉到對應位置。

  • 對於 long、float 和 double 型別的條件分支比較,會先執行比較指令返回一個整型值到運算元棧中後再執行 int 型別的條件跳轉指令。

  • 對於 boolean、byte、char、short,以及 int,則直接使用條件跳轉指令來完成。

比較條件轉移指令

指令 含義
if_icmpeq 比較棧頂兩個int的數值,相等時跳轉
if_icmpne 比較棧頂兩個int的數值,不等時跳轉
if_icmplt 比較棧頂兩個int的數值,前者小於後者時跳轉
if_icmple 比較棧頂兩個int的數值,前者小於等於後者時跳轉
if_icmpgt 比較棧頂兩個int的數值,前者小大於後者時跳轉
if_icmpge 比較棧頂兩個int的數值,前者大於等於後者時跳轉
if_acmpeq 比較棧頂兩個引用型別的大小,相等時跳轉
if_acmpne 比較棧頂兩個引用型別的大小,不等時跳轉

多分支轉移指令

  • tableswitch:要求多個條件分支值是連續的,它內部只存放起始值和終止值,以及若干個跳轉偏移量,透過給定的運算元 index,可以立即定位到跳轉偏移量位置,因此效率比較高
  • lookupswitch:內部存放著各個離散的 case-offset 對,每次執行都要搜尋全部的 case-offset 對,找到匹配的 case 值,並根據對應的 offset 計算跳轉地址,因此效率較低。

無條件轉移指令

  • goto 指令接收兩個位元組的運算元,共同組成一個帶符號的整數,用於指定指令的偏移量,指令執行的目的就是跳轉到偏移量給定的位置處。如果指令的偏移量特別大,超出了兩個位元組的範圍,可以使用指令 goto_w,接收 4 個位元組的運算元。

異常處理時的位元組碼指令


public static void main(String[] args) {
    try {
        int a = 1 / 0;
    } catch (ArithmeticException e) {
        System.out.println("算術異常");
    }
}
// 常數1入棧
 0 iconst_1
// 常數0入棧
 1 iconst_0
 2 idiv
// 將除法的結果儲存到區域性變數表中(這裡會發生異常,指令實際上不會執行)
 3 istore_1
// 在 try 塊的末尾,有一個 goto 指令跳過 catch 塊的程式碼,跳到第16條處,+12是相對於當前位置的偏移量
 4 goto 16 (+12)
// catch 塊的開始。如果捕獲到異常,將異常物件儲存到區域性變數表。
 7 astore_1

// 8~13指令執行 System.out.println("發生算術異常")。
 8 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
11 ldc #4 <算術異常>
13 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
    
16 return

異常表中的資訊表示當在位元組碼偏移量 0 到 4 之間發生 ArithmeticException 時,控制跳轉到偏移量 7,即 catch 塊的開始。

synchronized 的位元組碼指令


public void syncBlockMethod() {
    synchronized (this) {
    }
}
// 將區域性變數表中下標為 0 的 this 壓入運算元棧中
 0 aload_0
// 複製棧頂的 this 並重新入棧
 1 dup
// 取出棧頂的 this 放入區域性變數表中下標為 1 的位置
 2 astore_1
// 同步塊的開始
 3 monitorenter
//  將區域性變數表中下標為 1 的 this 壓入運算元棧中
 4 aload_1
// 同步塊的結束
 5 monitorexit
 6 goto 14 (+8)
 9 astore_2
10 aload_1
11 monitorexit
12 aload_2
13 athrow
14 return

相關文章