教你用Java位元組碼做點有趣的事

咖啡拿鐵發表於2018-07-20

0.寫在前面

為什麼會寫這篇文章呢?主要是之前調研過日誌脫敏相關的一些,具體可以參考LOG4j脫敏外掛如何編寫 裡面描述了日誌脫敏外掛編寫方法:

  • 直接在toString中修改程式碼,這種方法很麻煩,效率低,需要修改每一個要脫敏的類,或者寫個idea外掛自動修改toString(),這樣不好的地方在於所有編譯器都需要開個外掛,不夠通用。
  • 在編譯時期修改抽象語法樹修改toString()方法,就像類似Lombok一樣,這個之前調研過,開發難度較大,可能後會更新如何去寫。
  • 在載入的時候通過實現Instrumentation介面 asm庫,修改class檔案的位元組碼,但是有個比較麻煩的地方在於需要給jvm加上啟動引數 -javaagent:agentjarpath,這個已經實現了,但是實現後發現的確不夠通用。

期中二三兩個已經實現了,開發這個的確比較有趣,自己的知識面也得到了擴充套件,後續會通過寫4-5篇的文章,一步一步的帶大家如何去實現這些有趣的工具,學會了之後,通過大家豐富的想象力相信能實現更多有意思的東西。

0.1位元組碼能幹什麼

例如我這篇文章要介紹的通過修改位元組碼去實現日誌脫敏,其實就是修改toString的位元組碼: 可以看看怎麼用:

@Desensitized
public class StreamDemo1 {


    private User user;
    @DesFiled(MobileDesFilter.class)
    private String name;
    private String idCard;
    @DesFiled(AddressDesFilter.class)
    private List<String> mm;
    public static void main(String[] args) throws IOException {
        StreamDemo1 streamDemo1 = new StreamDemo1();
        streamDemo1.setUser(new User());
        streamDemo1.setName("18428368642");
        streamDemo1.setIdCard("22321321321");
        streamDemo1.setMm(Arrays.asList("北京是朝陽區打撒所大所大","北京是朝陽區打撒所大所大"));
        System.out.println(streamDemo1);
    }
    
    @Override
    public String toString() {
        return "StreamDemo1{" +
                "user=" + user +
                ", name='" + name + '\'' +
                ", idCard='" + idCard + '\'' +
                ", mm=" + mm +
                '}';
    }
}
複製程式碼

這個類很普通對吧,和其他的實體類,唯一的區別是多了一個註解: @DesFiled(MobileDesFilter.class),有了這個註解我們執行這個main方法:他會輸出:

StreamDemo1{user=bean.User@22d8cfe0, name='184****4777', idCard='22321321321', mm=[北京是朝陽區打*****, 北京是朝陽區打*****]}
複製程式碼

可以看見我們明明輸入的是不帶號的手機號,為什麼輸出缺帶號了呢,這就是操縱位元組碼的神奇。當然大家也可以自己擴充套件思維,你可以用他來做aop切面,當然cglib做切面的確也是操縱的位元組碼,你也可以用它來做你想讓它做的事

0.2語法樹

另一方面我也調研了lombok的實現,對此我發現修改抽象語法樹,似乎更加有趣,你可以想象,你平時是否重複的給每個方法列印入參出參,耗時耗力?你平時是否在為缺少關鍵的日誌而感到想罵人?你平時是否害怕用寫AOP用反射打日誌會影響效能?為了解決這個問題做了一個意思的工具slothLog,github地址:slothlog github https://github.com/lzggsimida123/slothlog.git (當然也求各位大佬們給點star,O(∩_∩)O哈哈~)。

@LogInfo
public class DemoService {
    public String hello(String name, int age){
        System.out.println(name + age + "hello");
        return name+age;
    }
    public static void main(String[] args) {
        DemoService demoService = new DemoService();
        demoService.hello("java", 100);
    }
}
複製程式碼

通過上面會輸出以下資訊,將方法的出參,入參都進行輸出,脫離了除錯時缺少日誌的苦惱

[INFO ] 2018-07-20 20:02:42,219 DemoService.main invoke start  args: {} 
[INFO ] 2018-07-20 20:02:42,220 DemoService.hello invoke start  name: java ,age: 100 
java100hello
[INFO ] 2018-07-20 20:02:42,221 DemoService.hello invoke end  name: java ,age: 100 , result: java100
複製程式碼

後續我會一步一步的教大家如何去完成一個類似Lombok的修改語法樹的框架,做更多有趣的事。

0.3關於本篇

如果你不喜歡上面這些東西,也彆著急,位元組碼是java的基礎,我覺得是所有Java程式設計師需要必備的,當然你也有必要了解一下。 本篇是系列的第一篇,這篇主要講的主要是位元組碼是什麼,通過對這篇的瞭解,也是後續章節的基礎。

1.什麼是位元組碼?

1.1機器碼

機器碼(machine code)顧名思義也就是,機器能識別的程式碼,也叫原生碼。機器碼是CPU可直接解讀的指令。機器碼與硬體等有關,不同的CPU架構支援的硬體碼也不相同。機器碼是和我們的底層硬體直接打交道,現在學的人也是逐漸的變少了,如果對這個感興趣的同學可以去學習一下彙編,彙編的指令會被翻譯成機器碼。

1.2位元組碼

位元組碼(Byte-code)是一種包含執行程式、由一序列 op 程式碼/資料對組成的二進位制檔案。位元組碼是程式的中間表示形式:介於人類可讀的原始碼和機器碼之間。它經常被看作是包含一個執行程式的二進位制檔案,更像一個物件模型。位元組碼被這樣叫是因為通常每個操作碼 是一位元組長,所以位元組碼的程度是根據一位元組來的。位元組碼也是由,一組操作碼組成,而操作碼實際上是對棧的操作,可以移走引數和地址空間,也可以放入結果。JAVA通過JIT(即時編譯)可以將位元組碼轉換為機器碼。

位元組碼的實現方式是通過編譯器和虛擬機器器。編譯器將原始碼編譯成位元組碼,特定平臺上的虛擬機器器將位元組碼轉譯為可以直接執行的指令。在java中一般是用Javac編譯原始檔變成位元組碼,也就是我們的class檔案。

從網路上找到了兩張圖片,下面是java原始碼編譯器生成位元組碼過程:

教你用Java位元組碼做點有趣的事

java虛擬機器執行引擎過程,這裡會分為兩個階段:

  • 普通的程式碼(非熱)都是走的位元組碼直譯器

  • 熱程式碼:多次呼叫的方法,多次執行的迴圈體,會被JIT優化成機器碼。

教你用Java位元組碼做點有趣的事

2.位元組碼執行

2.1JVM楨棧結構:

方法呼叫在JVM中轉換成的是位元組碼執行,位元組碼指令執行的資料結構就是棧幀(stack frame)。也就是在虛擬機器棧中的棧元素。虛擬機器會為每個方法分配一個棧幀,因為虛擬機器棧是LIFO(後進先出)的,所以當前執行緒正在活動的棧幀,也就是棧頂的棧幀,JVM規範中稱之為“CurrentFrame”,這個當前棧幀對應的方法就是“CurrentMethod”。位元組碼的執行操作,指的就是對當前棧幀資料結構進行的操作。

JVM的執行時資料區的結構如下圖:。

教你用Java位元組碼做點有趣的事

我們這裡主要討論棧幀的資料結構:有四個部分,區域性變數區,運算元棧,動態連結,方法的返回地址。

2.1.1區域性變數表:

區域性變數表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。在Java程式被編譯成Class檔案時,就在Code屬性中locals變數:

如下面程式碼反編譯後就能看見locals=5。

教你用Java位元組碼做點有趣的事

教你用Java位元組碼做點有趣的事

區域性變數的容量以變數槽(Slot)為最小單位,32位虛擬機器中一個Slot可以存放一個32位以內的資料型別(boolean、byte、char、short、int、float、reference(引用)和returnAddress八種)。

同時Slot對物件的引用會影響GC,(要是被引用,不會被回收)。

系統不會為區域性變數賦予初始值,也就是說不存在類變數那樣的準備階段。

虛擬機器是使用區域性變數表完成引數值到引數變數列表的傳遞過程的,如果是例項方法(非static),那麼區域性變數表的第0位索引的Slot預設是用於傳遞方法所屬物件例項的引用,在方法中通過this訪問。

我們上面的程式碼中是4個Int的solt加一個this 的solt所以就等於5。

2.1.2運算元棧

Java虛擬機器的解釋執行引擎被稱為"基於棧的執行引擎",其中所指的棧就是指-運算元棧。

運算元棧同區域性變數表一樣,也是編譯期間就能決定了其儲存空間(最大的單位長度),通過 Code屬性儲存在類或介面的位元組流中。運算元棧也是個LIFO棧。 它不是通過索引來訪問,而是通過標準的棧操作—壓棧和出棧—來訪問的。比如,如果某個指令把一個值壓入到運算元棧中,稍後另一個指令就可以彈出這個值來使用。

虛擬機器在運算元棧中儲存資料的方式和在區域性變數區中是一樣的:如int、long、float、double、reference和returnType的儲存。對於byte、short以及char型別的值在壓入到運算元棧之前,也會被轉換為int。

2.1.3動態連結

動態連結就是將符號引用所表示的方法,轉換成方法的直接引用。載入階段或第一次使用時轉化為直接引用的(將變數的訪問轉化為訪問這些變數的儲存結構所在的執行時記憶體位置)就叫做靜態解析。JVM的動態連結還支援執行期轉化為直接引用。也可以叫做Late Binding,晚期繫結。動態連結是java靈活OO的基礎結構。

注:

符號引用就是字串,這個字串包含足夠的資訊,以供實際使用時可以找到相應的位置。你比如說某個方法的符號引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。裡面有類的資訊,方法名,方法引數等資訊。

當第一次執行時,要根據字串的內容,到該類的方法表中搜尋這個方法。執行一次之後,符號引用會被替換為直接引用,下次就不用搜尋了。直接引用就是偏移量,通過偏移量虛擬機器可以直接在該類的記憶體區域中找到方法位元組碼的起始位置。重寫就是動態連結,過載就是靜態解析。

2.1.4方法返回地址

**方法正常退出,JVM執行引擎會恢復上層方法區域性變數表運算元棧並把返回值壓入呼叫者的棧幀的運算元棧,PC計數器的值就會調整到方法呼叫指令後面的一條指令。**這樣使得當前的棧幀能夠和呼叫者連線起來,並且讓呼叫者的棧幀的運算元棧繼續往下執行。   方法的異常呼叫完成,如果異常沒有被捕獲住,或者遇到athrow位元組碼指令顯示丟擲,那麼就沒有返回值給呼叫者。

2.2位元組碼指令集

2.2.1載入和儲存指令

載入和儲存指令用於將資料從棧幀的區域性變數表和運算元棧之間來回傳輸。

1)將一個區域性變數載入到運算元棧的指令包括:iload,iload_,lload、lload、float、 fload_、dload、dload_,aload、aload。

2)將一個數值從運算元棧儲存到區域性變數表的指令:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_

3)將常量載入到運算元棧的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_

4)區域性變數表的訪問索引指令:wide

2.2.2運算指令

算術指令用於對兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。

1)加法指令:iadd,ladd,fadd,dadd

2)減法指令:isub,lsub,fsub,dsub

3)乘法指令:imul,lmul,fmul,dmul

4)除法指令:idiv,ldiv,fdiv,ddiv

5)求餘指令:irem,lrem,frem,drem

6)取反指令:ineg,leng,fneg,dneg

7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr

8)按位或指令:ior,lor

9)按位與指令:iand,land

10)按位異或指令:ixor,lxor

11)區域性變數自增指令:iinc

12)比較指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

Java虛擬機器沒有明確規定整型資料溢位的情況,但規定了處理整型資料時,只有除法和求餘指令出現除數為0時會導致虛擬機器丟擲異常。

Java虛擬機器要求在浮點數運算的時候,所有結果否必須舍入到適當的精度,如果有兩種可表示的形式與該值一樣,會優先選擇最低有效位為零的。稱之為最接近數舍入模式。

浮點數向整數轉換的時候,Java虛擬機器使用IEEE 754標準中的向零舍入模式,這種模式舍入的結果會導致數字被截斷,所有小數部分的有效位元組會被丟掉。

2.2.3型別轉換指令

型別轉換指令將兩種Java虛擬機器數值型別相互轉換,這些操作一般用於實現使用者程式碼的顯式型別轉換操作。JVM直接就支援寬化型別轉換(小範圍型別向大範圍型別轉換):

1.int型別到long,float,double型別

2.long型別到float,double型別

3.float到double型別

但在處理窄化型別轉換時,必須顯式使用轉換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。將int 或 long 窄化為整型T的時候,僅僅簡單的把除了低位的N個位元組以外的內容丟棄,N是T的長度。這有可能導致轉換結果與輸入值有不同的正負號。

在將一個浮點值窄化為整數型別T(僅限於 int 和 long 型別),將遵循以下轉換規則:

1)如果浮點值是NaN , 那轉換結果就是int 或 long 型別的0

2)如果浮點值不是無窮大,浮點值使用IEEE 754 的向零舍入模式取整,獲得整數v, 如果v在T表示範圍之內,那就是v

3)否則,根據v的符號, 轉換為T 所能表示的最大或者最小正數

2.2.4物件建立和訪問指令

雖然類例項和陣列都是物件,Java虛擬機器對類例項和陣列的建立與操作使用了不同的位元組碼指令。

1)建立例項的指令:new

2)建立陣列的指令:newarray,anewarray,multianewarray

3)訪問欄位指令:getfield,putfield,getstatic,putstatic

4)把陣列元素載入到運算元棧指令:baload,caload,saload,iaload,laload,faload,daload,aaload

5)將運算元棧的數值儲存到陣列元素中執行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore

6)取陣列長度指令:arraylength JVM支援方法級同步和方法內部一段指令序列同步,這兩種都是通過moniter實現的。

7)檢查例項型別指令:instanceof,checkcast

2.2.5運算元棧管理指令

如同操作一個普通資料結構中的堆疊那樣,Java虛擬機器提供了一些用於直接操作操作舒展的指令,包括:

1)將運算元棧的棧頂一個或兩個元素出棧:pop、pop2

2)複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。

3)將棧最頂端的兩個數值互換:swap

2.2.6控制轉移指令

讓JVM有條件或無條件從指定指令而不是控制轉移指令的下一條指令繼續執行程式。控制轉移指令包括:

1)條件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等

2)複合條件分支:tableswitch,lookupswitch

3)無條件分支:goto,goto_w,jsr,jsr_w,ret

JVM中有專門的指令集處理int和reference型別的條件分支比較操作,為了可以無明顯標示一個實體值是否是null,有專門的指令檢測null 值。boolean型別和byte型別,char型別和short型別的條件分支比較操作,都使用int型別的比較指令完成,而 long,float,double條件分支比較操作,由相應型別的比較運算指令,運算指令會返回一個整型值到運算元棧中,隨後再執行int型別的條件比較操作完成整個分支跳轉。各種型別的比較都最終會轉化為int型別的比較操作。

2.2.7方法呼叫和返回指令

invokevirtual指令:呼叫物件的例項方法,根據物件的實際型別進行分派(虛擬機器分派)。

invokeinterface指令:呼叫介面方法,在執行時搜尋一個實現這個介面方法的物件,找出合適的方法進行呼叫。

invokespecial:呼叫需要特殊處理的例項方法,包括例項初始化方法,私有方法和父類方法

invokestatic:呼叫類方法(static)

方法返回指令是根據返回值的型別區分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn(long),freturn,drturn(double)和areturn(引用地址),另外一個return供void方法,例項初始化方法,類和介面的類初始化i方法使用。

2.2.8異常處理指令

在Java程式中顯式丟擲異常的操作(throw語句)都有athrow 指令來實現,除了用throw 語句顯示丟擲異常情況外,Java虛擬機器規範還規定了許多執行時異常會在其他Java虛擬機器指令檢測到異常狀況時自動丟擲。在Java虛擬機器中,處理異常不是由位元組碼指令來實現的,而是採用異常表來完成的。

2.2.9同步指令

方法級的同步是隱式的,無需通過位元組碼指令來控制,它實現在方法呼叫和返回操作中。虛擬機器從方法常量池中的方法標結構中的 ACC_SYNCHRONIZED標誌區分是否是同步方法。方法呼叫時,呼叫指令會檢查該標誌是否被設定,若設定,執行執行緒持有moniter,然後執行方法,最後完成方法時釋放moniter。同步一段指令集序列,通常由synchronized塊標示,JVM指令集中有monitorenter和monitorexit來支援synchronized語義。

大多數的指令有字首和(或)字尾來表明其運算元的型別。

3.位元組碼例項分析

這一節將給大家分析如何一步一步的分析位元組碼。

3.1原始碼

有如下簡單程式碼,下面程式碼是一個簡單的demo,有一個常量,有一個類成員變數,同時方法有三個,一個構造方法,一個get(),一個靜態main方法,用來輸出資訊。

package java8;

public class ByteCodeDemo {
    private static final String name = "xiaoming";
    
    private int age;

    public ByteCodeDemo(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public static void main(String[] args) {
        ByteCodeDemo byteCodeDeomo = new ByteCodeDemo(12);
        System.out.println("name:" + name + "age:" + byteCodeDeomo.getAge());
    }
}
複製程式碼

3.2.反編譯

用命令列找到我們這段程式碼所在的路徑,輸入如下命令:

javac ByteCodeDemo.java

javap -p -v ByteCodeDemo
複製程式碼

有關Javap命令可以用help或者參考javap命令,我們這裡用的-p,-v輸出所有類和成員資訊,以及附加資訊(檔案路徑,檔案大小,常量池等等)

3.3.得到如下資訊

Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class
  Last modified 2018-5-8; size 861 bytes
  MD5 checksum d225c0249912bec4b11c41a0a52e6418
  Compiled from "ByteCodeDemo.java"
public class java8.ByteCodeDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#32         // java8/ByteCodeDemo.age:I
   #3 = Class              #33            // java8/ByteCodeDemo
   #4 = Methodref          #3.#34         // java8/ByteCodeDemo."<init>":(I)V
   #5 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Class              #37            // java/lang/StringBuilder
   #7 = Methodref          #6.#31         // java/lang/StringBuilder."<init>":()V
   #8 = String             #38            // name:xiaomingage:
   #9 = Methodref          #6.#39         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #10 = Methodref          #3.#40         // java8/ByteCodeDemo.getAge:()I
  #11 = Methodref          #6.#41         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #12 = Methodref          #6.#42         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #13 = Methodref          #43.#44        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #14 = Class              #45            // java/lang/Object
  #15 = Utf8               name
  #16 = Utf8               Ljava/lang/String;
  #17 = Utf8               ConstantValue
  #18 = String             #46            // xiaoming
  #19 = Utf8               age
  #20 = Utf8               I
  #21 = Utf8               <init>
  ....省略部分
  #58 = Utf8               (Ljava/lang/String;)V
{
  private static final java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String xiaoming

  private int age;
    descriptor: I
    flags: ACC_PRIVATE

  public java8.ByteCodeDemo(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #2                  // Field age:I
         9: return
      LineNumberTable:
        line 18: 0
        line 19: 4
        line 20: 9

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 23: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #3                  // class java8/ByteCodeDemo
         3: dup
         4: bipush        12
         6: invokespecial #4                  // Method "<init>":(I)V
         9: astore_1
        10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: new           #6                  // class java/lang/StringBuilder
        16: dup
        17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        20: ldc           #8                  // String name:xiaomingage:
        22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: aload_1
        26: invokevirtual #10                 // Method getAge:()I
        29: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        38: return
      LineNumberTable:
        line 27: 0
        line 28: 10
        line 29: 38
}
SourceFile: "ByteCodeDemo.java"
複製程式碼

如果你是第一次用javap,那你一定會覺得這個是啥又臭又長,彆著急下面我會一句一句給你翻譯,這裡你需要對照上面的位元組碼指令,一步一步的帶你翻譯。

3.4.附加資訊

Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class //輸出了我們的class檔案的完整路徑
  Last modified 2018-5-8; size 861 bytes //以及class檔案修改時間以及大小
  MD5 checksum d225c0249912bec4b11c41a0a52e6418 //md5校驗和
  Compiled from "ByteCodeDemo.java" //從哪個檔案編譯而來
public class java8.ByteCodeDemo 
  minor version: 0
  major version: 52 //java主版本  major_version.minor_version 組成我們的版本號52.0
  flags: ACC_PUBLIC, ACC_SUPER //public,ACC_SUPER用於相容早期編譯器,新編譯器都設定該標記,以在使用 invokespecial指令時對子類方法做特定處理。
Constant pool:
   #1 = Methodref          #14.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#32         // java8/ByteCodeDemo.age:I
   #3 = Class              #33            // java8/ByteCodeDemo
   .........
複製程式碼

部分資訊在後面已經註釋解釋, 我們主要來說一下我們的Constant pool,常量池:

在Java位元組碼中,有一個常量池,用來存放不同型別的常量。由於Java設計的目的之一就是位元組碼需要經網路傳輸的,因而位元組碼需要比較緊湊,以減少網路傳輸的流量和時間。常量池的存在則可以讓一些相同型別的值通過索引(引用)的方式從常量池中找到,而不是在不同地方有不同拷貝,縮減了位元組碼的大小。

tag中表示的資料型別,有如下11種,:

  • CONSTANT_Class_info

  • CONSTANT_Integer_info

  • CONSTANT_Long\info

  • CONSTANT_Float_info

  • CONSTANT_Double_info

  • CONSTANT_String_info

  • CONSTANT_Fieldref_info

  • CONSTANT_Methodref_info

  • CONSTANT_InterfaceMethodref_info

  • CONSTANT_NameAndType_info

  • CONSTANT_Utf8_info

注:在Java位元組碼中,所有boolean、byte、char、short型別都是用int型別存放,因而在常量池中沒有和它們對應的項。 有關常量池的介紹可以參照這裡:

http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html

3.5.main方法分析

這裡把main方法單獨複製了出來,每一句話都進行了解釋。

在看下面之前,可以自己嘗試一下是否能將main方法位元組碼看懂

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V //方法描述,入參是String,返回是void
    flags: ACC_PUBLIC, ACC_STATIC 
    Code:
      stack=3, locals=2, args_size=1 //棧深最大3,區域性變數2,args_size入參是1(如果是實體方法會把this也算入參)
         0: new           #3                  // class java8/ByteCodeDemo new指令建立物件,這裡引用了常量池的class 所以這裡一共佔了三行 2個位元組是class 
         //一個位元組是new,所以下個行號是 0+3 = 3 並把當前申請的空間地址放到棧頂
         3: dup 															//將棧頂cpoy一份再次放入棧頂,也就是我們上面的空間地址
         4: bipush        12									//取常量12放入棧空間
         6: invokespecial #4                  // Method "<init>":(I)V //執行初始化方法這個時候會用到4的棧頂,和3的棧頂,彈出
         9: astore_1													//將棧頂放入區域性變數,也就是0的空間地址,這個時候棧是空的
        10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream; //獲取這個方法地址到棧頂
        13: new           #6                  // class java/lang/StringBuilder 把新開闢的空間地址放到棧頂
        16: dup																//複製一份
        17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V //彈出棧頂
        20: ldc           #8                  // String name:xiaomingage://取常量到棧頂
        22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//彈出棧頂兩個元素,壓入StringBuilder的引用
        25: aload_1														// 把區域性變數,也就是我們剛才的空間地址壓入
        26: invokevirtual #10                 // Method getAge:()I //彈出棧頂,獲取年齡,把年齡壓入棧頂
        29: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//彈出棧頂兩個元素,壓入StringBuilder
        32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;//彈出棧頂兩個元素,壓入toString
        35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V//彈出棧頂兩個元素,此時棧空
        38: return //返回
      LineNumberTable: //位元組碼偏移量到原始碼行號之間的聯絡
        line 29: 0 
        line 30: 10
        line 31: 38
}
複製程式碼

教你用Java位元組碼做點有趣的事
教你用Java位元組碼做點有趣的事

思考:這裡看懂了之後,大家可以自己嘗試下自己寫個稍微複雜的位元組碼,然後進行理解,加深一下映像。

最後

下一篇位元組碼之ASM,下一篇將會給大家詳細講解如何通過asm去操作位元組碼,以及如何去實現我們上面的功能,喜歡這一系列可以關注公眾號,不丟失文章

如果大家覺得這篇文章對你有幫助,或者想提前獲取後續章節文章,或者你有什麼疑問想提供1v1免費vip服務,都可以關注我的公眾號,關注即可免費領取上百G最新java學習資料視訊,以及最新面試資料,你的關注和轉發是對我最大的支援,O(∩_∩)O:

教你用Java位元組碼做點有趣的事

相關文章