帶你閱讀字節碼


什么是字節碼?

java bytecode 由單字節(byte)的指令組成,理論上最多支持 256 個操作碼(opcode),實際上 Java 只使用了 200 個左右的操作碼,還有一些操作碼則保留給調試操作。

根據指令的性質,主要分為四大類:

  1. 棧操作指令,包括與局部變量交互的指令。
  2. 程序流程控制指令。
  3. 對象操作指令,包括方法調用指令。
  4. 算術運算以及類型轉換指令。

一、如何生成字節碼?

其實字節碼就是 class 文件,如何生成 class 文件呢?就是使用 javac 命令。

# 生成字節碼
javac -g:vars HelloByteCode.java

# 查看字節碼
javap -verbose HelloByteCode

二、字節碼指令

對於大部分為與數據類型相關的字節碼指令,他們的操作碼助記符中都有特殊的字符來表明專門為哪種數據類型服務:i代表對int類型的數據操作、l代表long、s代表short、b代表byte、c代表char、f代表float、d代表double、a代表reference。

1、加載和存儲指令

  • 加載局部變量到操作棧:iloadiload_<n>lloadlload_<n>floadfload_<n>dloaddload_<n>aloadaload_<n>
  • 加載常量到操作棧:bipushsipushldcldc_wldc2_waconst_nulliconst_m1iconst_<i>lconst_<l>fconst_<f>dconst_<d>
  • 將操作數棧存儲到局部變量表:istoreistore_<n>lstorelstore_<n>fstorefstore_<n>dstoredstore_<n>astoreastore_<n>

2、運算指令(運算結果會自動入棧)

  • 加法指令:iaddladdfadddadd
  • 減法指令:isublsubfsubdsub
  • 乘法指令:imullmulfmuldmul
  • 除法指令:idivldivfdivddiv
  • 求余指令:iremlremfremdrem
  • 取反指令:ineglnegfnegdneg
  • 位移指令:ishlishriushrlshllshrlushr
  • 按位或指令:iorlor
  • 按位與指令:iandland
  • 按位異或指令:ixorlxor
  • 局部變量自增指令:iinc
  • 比較指令:dcmpgdcmplfcmpgfcmpllcmp

3、類型轉換

JVM 對類型寬化自然支持,並不需要執行指令,但是對類型窄化需要執行指令:i2bi2ci2sl2if2if2ld2id2ld2f

4、對象的創建及訪問

  • 創建類實例:new
  • 訪問類字段或實例字段:getfieldputfieldgetstaticputstatic

5、數組

  • 創建數組:newarraynewwarraymultianewarray
  • 加載數組到操作數棧:baloadcaloadsaloadialoadlaloadfaloaddaloadaaload
  • 將操作數棧存儲到數組元素:bastorecastoresastoreiastorefastoredastoreaastore
  • 取數組長度的指令:arraylength

6、流程控制

  • 條件判斷:ifeqifltifleifneifgtifgeifnullifnonnullif_icmpeqif_icmpneif_icmplt, if_icmpgtif_icmpleif_icmpgeif_acmpeqif_acmpne
  • 復合條件分支:tableswitchlookupswitch
  • 無條件分支:gotogoto_wjsrjsr_wret

7、方法調用和返回指令(調用之后數據依然在操作數棧中)

  • invokevirtual:用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
  • invokeinterface:用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
  • invokespecial:用於調用一些需要特殊處理的實例方法,包括實例初始化方法(§2.9)、私有方法和父類方法。
  • invokestatic:指令用於調用類方法(static方法)。

8、返回值指令

  • ireturn(當返回值是boolean、byte、char、short和int類型時使用)
  • lreturn
  • freturn
  • dreturn
  • areturn
  • 另外還有一條return指令供聲明為void的方法、實例初始化方法、類和接口的類初始化方法使用

三、閱讀字節碼文件

閱讀字節碼文件之前,先了解一下 JVM 有哪些常用的操作碼(opcode),官方文檔

例子:

public class HelloByteCode {
    public static void main(String[] args) {
        System.out.println("Hello ByteCode.");
		
        // 基本類型定義
        byte b1 = 100;
        short s1 = 30000;
        int i1 = 50;
        int i2 = 200;
        int i3 = 40000;
        long l1 = 300;
        long l2 = 1000000;
        float f1 = 10.5F;
        double d1 = 20.5D;
        // 引用類型定義
        String str1 = "Hello";

        // 四則運算
        int sum = i1 + i2;
        int sub = i1 - i2;
        int mul = i1 * i2;
        int dvi = i2 / i1;

        if (sum == 0) {
            System.out.println("sum == 0");
        }

        for (int i = 0; i < 3; i++) {
            System.out.println("index:" + i);
        }

    }

}

編譯、查看、翻譯字節碼:

➜  java javac -g:vars com/snailwu/course/code/HelloByteCode.java
➜  java javap -verbose com/snailwu/course/code/HelloByteCode
// class 文件的位置
Classfile /Users/wu/GitLab/java-course-code/src/main/java/com/snailwu/course/code/HelloByteCode.class
  // 最后修改時間,文件大小
  Last modified 2021-6-28; size 1243 bytes
  // MD5
  MD5 checksum 944f6523f8b0126fb7d76753c9193800
// 全類名
public class com.snailwu.course.code.HelloByteCode
  // 最低最高版本號
  minor version: 0
  major version: 52
  // 類的修飾符
  flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
   #1 = Methodref          #22.#58        // java/lang/Object."<init>":()V
   #2 = Fieldref           #59.#60        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #61            // Hello ByteCode.
   #4 = Methodref          #62.#63        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Integer            40000
   #6 = Long               300l
   #8 = Long               1000000l
  #10 = Float              10.5f
  #11 = Double             20.5d
  #13 = String             #64            // Hello
  #14 = String             #65            // sum == 0
  #15 = Class              #66            // java/lang/StringBuilder
  #16 = Methodref          #15.#58        // java/lang/StringBuilder."<init>":()V
  #17 = String             #67            // index:
  #18 = Methodref          #15.#68        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #19 = Methodref          #15.#69        // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #20 = Methodref          #15.#70        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #21 = Class              #71            // com/snailwu/course/code/HelloByteCode
  #22 = Class              #72            // java/lang/Object
  #23 = Utf8               <init>
  #24 = Utf8               ()V
  #25 = Utf8               Code
  #26 = Utf8               LocalVariableTable
  #27 = Utf8               this
  #28 = Utf8               Lcom/snailwu/course/code/HelloByteCode;
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               i
  #32 = Utf8               I
  #33 = Utf8               args
  #34 = Utf8               [Ljava/lang/String;
  #35 = Utf8               b1
  #36 = Utf8               B
  #37 = Utf8               s1
  #38 = Utf8               S
  #39 = Utf8               i1
  #40 = Utf8               i2
  #41 = Utf8               i3
  #42 = Utf8               l1
  #43 = Utf8               J
  #44 = Utf8               l2
  #45 = Utf8               f1
  #46 = Utf8               F
  #47 = Utf8               d1
  #48 = Utf8               D
  #49 = Utf8               str1
  #50 = Utf8               Ljava/lang/String;
  #51 = Utf8               sum
  #52 = Utf8               sub
  #53 = Utf8               mul
  #54 = Utf8               dvi
  #55 = Utf8               StackMapTable
  #56 = Class              #34            // "[Ljava/lang/String;"
  #57 = Class              #73            // java/lang/String
  #58 = NameAndType        #23:#24        // "<init>":()V
  #59 = Class              #74            // java/lang/System
  #60 = NameAndType        #75:#76        // out:Ljava/io/PrintStream;
  #61 = Utf8               Hello ByteCode.
  #62 = Class              #77            // java/io/PrintStream
  #63 = NameAndType        #78:#79        // println:(Ljava/lang/String;)V
  #64 = Utf8               Hello
  #65 = Utf8               sum == 0
  #66 = Utf8               java/lang/StringBuilder
  #67 = Utf8               index:
  #68 = NameAndType        #80:#81        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #69 = NameAndType        #80:#82        // append:(I)Ljava/lang/StringBuilder;
  #70 = NameAndType        #83:#84        // toString:()Ljava/lang/String;
  #71 = Utf8               com/snailwu/course/code/HelloByteCode
  #72 = Utf8               java/lang/Object
  #73 = Utf8               java/lang/String
  #74 = Utf8               java/lang/System
  #75 = Utf8               out
  #76 = Utf8               Ljava/io/PrintStream;
  #77 = Utf8               java/io/PrintStream
  #78 = Utf8               println
  #79 = Utf8               (Ljava/lang/String;)V
  #80 = Utf8               append
  #81 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #82 = Utf8               (I)Ljava/lang/StringBuilder;
  #83 = Utf8               toString
  #84 = Utf8               ()Ljava/lang/String;
{
  // 空的構造方法
  public com.snailwu.course.code.HelloByteCode();
    // 方法的修飾符:包括參數及返回值。
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      // 為什么args_size為1?非靜態方法默認第一個參數為 this 對象。參看 LocalVariableTable
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/snailwu/course/code/HelloByteCode;

  // main 方法
  public static void main(java.lang.String[]);
    // 方法的修飾符:包括參數及返回值。
    descriptor: ([Ljava/lang/String;)V
    // 修飾符
    flags: ACC_PUBLIC, ACC_STATIC
    // 代碼區
    Code:
      // 第一個參數是 args
      stack=3, locals=19, args_size=1
        // 獲取 PrintStream 實例
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // 將常量加載到操作數棧
         3: ldc           #3                  // String Hello ByteCode.
        // 調用方法進行輸出
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        // 將 100 壓入操作數棧
         8: bipush        100
        // 將操作數棧棧頂元素存到本地變量slot為1的位置
        10: istore_1
        // 將 30000 壓棧
        11: sipush        30000
        // 將棧頂元素存到slot為2的位置
        14: istore_2
        // 將 50 壓棧
        15: bipush        50
        // 將棧頂元素存到slot為3的位置
        17: istore_3
        // 將 200 壓棧
        18: sipush        200
        // 將棧頂元素存到slot為4的位置
        21: istore        4
        // 將 40000 壓棧
        23: ldc           #5                  // int 40000
        // 將棧頂元素存到slot為5的位置
        25: istore        5
        // 將 300 壓棧,long占用兩個slot
        27: ldc2_w        #6                  // long 300l
        // 將棧頂元素存到slot為6、7的位置
        30: lstore        6
        // 將 1000000 壓棧,long占用兩個slot
        32: ldc2_w        #8                  // long 1000000l
        // 將棧頂元素存到slot為8、9的位置
        35: lstore        8
        // 將 10.5 壓棧
        37: ldc           #10                 // float 10.5f
        // 將棧頂元素存到slot為10的位置
        39: fstore        10
        // 將 20.5 壓棧
        41: ldc2_w        #11                 // double 20.5d
        // 將棧頂元素存到slot為11、12的位置
        44: dstore        11
        // 將 Hello 壓棧
        46: ldc           #13                 // String Hello
        / 將棧頂元素存到slot為13的位置
        48: astore        13
        // 加載slot為3處的值到棧頂
        50: iload_3
        // 加載slot為4處的值到棧頂
        51: iload         4
        // 將棧頂兩個元素彈出,進行相加,然后結果存入棧頂
        53: iadd
        // 將棧頂元素存入slot為14的位置
        54: istore        14
        // 加載slot為3處的值到棧頂
        56: iload_3
        // 加載slot為4處的值到棧頂
        57: iload         4
        // 將棧頂兩個元素彈出,進行相減,然后結果存入棧頂
        59: isub
        // 將棧頂元素存入slot為15的位置
        60: istore        15
        // 加載slot為3處的值到棧頂
        62: iload_3
        // 加載slot為4處的值到棧頂
        63: iload         4
        // 將棧頂兩個元素彈出,進行相乘,然后結果存入棧頂
        65: imul
        // 將棧頂元素存入slot為16的位置
        66: istore        16
        // 加載slot為4處的值到棧頂
        68: iload         4
        // 加載slot為3處的值到棧頂
        70: iload_3
        // 將棧頂兩個元素彈出,進行相除,然后結果存入棧頂
        71: idiv
        // 將棧頂元素存入slot為17的位置
        72: istore        17
        // 加載slot為14處的值到棧頂
        74: iload         14
        // 如果棧頂元素不等於0,跳轉到偏移量為87的位置往下執行
        76: ifne          87
        // 如果棧頂元素等於0,執行76到87之間的代碼
        // 79到84:輸出 "sum == 0"
        79: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        82: ldc           #14                 // String sum == 0
        84: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        // 將 0 壓棧
        87: iconst_0
        // 將棧頂元素存入slot為18的位置
        88: istore        18
        // 加載slot為18處的值到棧頂
        90: iload         18
        // 將 3 壓入棧頂
        92: iconst_3
        // 彈出棧頂兩個元素進行比較,如果 val1(第一個彈出的元素:3) >= val2(第二個彈出的元素:0) 則跳轉到偏移量為128的位置往下執行
        93: if_icmpge     128
        // 獲取 PrintStream 實例
        96: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // new 關鍵字,新建對象 StringBuilder,並將引用壓入棧頂
        99: new           #15                 // class java/lang/StringBuilder
        // 復制棧頂的元素並壓入棧頂,就是把 StringBuilder 的引用復制一份,invokespecial 會消耗一個 引用,如果不復制,后面的 append 就沒有引用可以使用了
       102: dup
       // 執行 StringBuilder 的初始化方法,消耗棧頂的一個 StringBuilder 對象引用,返回值是 void,所以這個引用消耗了就沒了
       103: invokespecial #16                 // Method java/lang/StringBuilder."<init>":()V
       // 將 "index" 壓入棧頂
       106: ldc           #17                 // String index:
       // 彈出棧頂的 StringBuilder 引用執行 append 方法,執行結果(返回值:StringBuilder類型的)壓入棧頂
       108: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       // 將slot為18處的值加載到棧頂
       111: iload         18
       // 彈出棧頂的 StringBuilder 引用執行 append 方法,執行結果(返回值:StringBuilder類型的)壓入棧頂
       113: invokevirtual #19                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
       // 彈出棧頂的 StringBuilder 引用執行 String 方法,執行結果(返回值:String類型的)壓入棧頂
       116: invokevirtual #20                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       // 彈出棧頂元素,執行 println 方法
       119: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       // slot為18出的值自增1
       122: iinc          18, 1
       // 跳轉到偏移量為90的位置繼續執行
       125: goto          90
       // 執行返回
       128: return
      // 本地變量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           90      38    18     i   I
            0     129     0  args   [Ljava/lang/String;
           11     118     1    b1   B
           15     114     2    s1   S
           18     111     3    i1   I
           23     106     4    i2   I
           27     102     5    i3   I
           32      97     6    l1   J
           37      92     8    l2   J
           41      88    10    f1   F
           46      83    11    d1   D
           50      79    13  str1   Ljava/lang/String;
           56      73    14   sum   I
           62      67    15   sub   I
           68      61    16   mul   I
           74      55    17   dvi   I
      // 棧表
      StackMapTable: number_of_entries = 3
        frame_type = 255 /* full_frame */
          offset_delta = 87
          locals = [ class "[Ljava/lang/String;", int, int, int, int, int, long, long, float, double, class java/lang/String, int, int, int, int ]
          stack = []
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 37
}

是不是字節碼文件也很容易閱讀。

四、指令總結

每一個線程都有一個保存幀的棧。在每一個方法調用的時候創建一個幀。一個幀包括了三個部分:操作棧,局部變量數組,和一個對當前方法所屬類的常量池的引用。

局部變量數組也被稱之為局部變量表,它包含了方法的參數,也用於保存一些局部變量的值。參數值得存放總是在局部變量數組的index0開始的。如果當前幀是由構造函數或者實例方法創建的,那么該對象引用將會存放在location0處,然后才開始存放其余的參數。

局部變量表的大小由編譯時決定,同時也依賴於局部變量的數量和一些方法的大小。操作棧是一個(LIFO)棧,用於壓入和取出值,其大小也在編譯時決定。某些opcode指令將值壓入操作棧,其余的opcode指令將操作數取出棧。使用它們后再把結果壓入棧。操作棧也用於接收從方法中返回的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM