教你用Java字節碼作點有趣的事

0.寫在前面

爲何會寫這篇文章呢?主要是以前調研過日誌脫敏相關的一些,具體能夠參考LOG4j脫敏插件如何編寫
裏面描述了日誌脫敏插件編寫方法:html

  • 直接在toString中修改代碼,這種方法很麻煩,效率低,須要修改每個要脫敏的類,或者寫個idea插件自動修改toString(),這樣很差的地方在於全部編譯器都須要開個插件,不夠通用。
  • 在編譯時期修改抽象語法樹修改toString()方法,就像相似Lombok同樣,這個以前調研過,開發難度較大,可能後會更新如何去寫。
  • 在加載的時候經過實現Instrumentation接口 asm庫,修改class文件的字節碼,可是有個比較麻煩的地方在於須要給jvm加上啓動參數 -javaagent:agentjarpath,這個已經實現了,可是實現後發現的確不夠通用。

期中二三兩個已經實現了,開發這個的確比較有趣,本身的知識面也獲得了擴展,後續會經過寫4-5篇的文章,一步一步的帶你們如何去實現這些有趣的工具,學會了以後,經過你們豐富的想象力相信能實現更多有意思的東西。java

0.1字節碼能幹什麼

例如我這篇文章要介紹的經過修改字節碼去實現日誌脫敏,其實就是修改toString的字節碼:
能夠看看怎麼用:git

@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作切面的確也是操縱的字節碼,你也能夠用它來作你想讓它作的事github

0.2語法樹

另外一方面我也調研了lombok的實現,對此我發現修改抽象語法樹,彷佛更加有趣,你能夠想象,你平時是否重複的給每一個方法打印入參出參,耗時耗力?你平時是否在爲缺乏關鍵的日誌而感到想罵人?你平時是否懼怕用寫AOP用反射打日誌會影響性能?爲了解決這個問題作了一個意思的工具slothLog,github地址:slothlog github https://github.com/lzggsimida... (固然也求各位大佬們給點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架構支持的硬件碼也不相同。機器碼是和咱們的底層硬件直接打交道,如今學的人也是逐漸的變少了,若是對這個感興趣的同窗能夠去學習一下彙編,彙編的指令會被翻譯成機器碼。app

1.2字節碼

字節碼(Byte-code)是一種包含執行程序、由一序列 op 代碼/數據對組成的二進制文件。字節碼是程序的中間表示形式:介於人類可讀的源碼和機器碼之間。它常常被看做是包含一個執行程序的二進制文件,更像一個對象模型。字節碼被這樣叫是由於一般每一個操做碼 是一字節長,因此字節碼的程度是根據一字節來的。字節碼也是由,一組操做碼組成,而操做碼其實是對棧的操做,能夠移走參數和地址空間,也能夠放入結果。JAVA經過JIT(即時編譯)能夠將字節碼轉換爲機器碼。

字節碼的實現方式是經過編譯器和虛擬機器。編譯器將源碼編譯成字節碼,特定平臺上的虛擬機器將字節碼轉譯爲能夠直接執行的指令。在java中通常是用Javac編譯源文件變成字節碼,也就是咱們的class文件。

從網絡上找到了兩張圖片,下面是java源碼編譯器生成字節碼過程:

java虛擬機執行引擎過程,這裏會分爲兩個階段:

  • 普通的代碼(非熱)都是走的字節碼解釋器
  • 熱代碼:屢次調用的方法,屢次執行的循環體,會被JIT優化成機器碼。

2.字節碼執行

2.1JVM楨棧結構:

方法調用在JVM中轉換成的是字節碼執行,字節碼指令執行的數據結構就是棧幀(stack frame)。也就是在虛擬機棧中的棧元素。虛擬機會爲每一個方法分配一個棧幀,由於虛擬機棧是LIFO(後進先出)的,因此當前線程正在活動的棧幀,也就是棧頂的棧幀,JVM規範中稱之爲「CurrentFrame」,這個當前棧幀對應的方法就是「CurrentMethod」。字節碼的執行操做,指的就是對當前棧幀數據結構進行的操做。

JVM的運行時數據區的結構以下圖:。

咱們這裏主要討論棧幀的數據結構:有四個部分,局部變量區,操做數棧,動態連接,方法的返回地址。

2.1.1局部變量表:

局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在Code屬性中locals變量:

以下面代碼反編譯後就能看見locals=5。

局部變量的容量以變量槽(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、dup二、dup_x一、dup2_x一、dup_x二、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語義。

大多數的指令有前綴和(或)後綴來代表其操做數的類型。以下表

<tbody><tr class="originHeader"><th width="68px" data-colwidth="68" style="min-width: 68px; max-width: 68px;"><p><span style="color: rgb(221, 17, 68);">前/後綴</span></p></th><th width="129px" data-colwidth="129" style="min-width: 129px; max-width: 129px;"><p><span style="color: rgb(221, 17, 68);">操做數類型</span></p></th></tr><tr><td width="68px" data-colwidth="68"><p>i</p></td><td width="129px" data-colwidth="129"><p>整數</p></td></tr><tr><td width="68px" data-colwidth="68"><p>l</p></td><td width="129px" data-colwidth="129"><p>長整數</p></td></tr><tr><td width="68px" data-colwidth="68"><p>s</p></td><td width="129px" data-colwidth="129"><p>短整數</p></td></tr><tr><td width="68px" data-colwidth="68"><p>b</p></td><td width="129px" data-colwidth="129"><p>字節</p></td></tr><tr><td width="68px" data-colwidth="68"><p>c</p></td><td width="129px" data-colwidth="129"><p>字符</p></td></tr><tr><td width="68px" data-colwidth="68"><p>f</p></td><td width="129px" data-colwidth="129"><p>單精度浮點數</p></td></tr><tr><td width="68px" data-colwidth="68"><p>d</p></td><td width="129px" data-colwidth="129"><p>雙精度浮點數</p></td></tr><tr><td width="68px" data-colwidth="68"><p>z</p></td><td width="129px" data-colwidth="129"><p>布爾值</p></td></tr><tr><td width="68px" data-colwidth="68"><p>a</p></td><td width="129px" data-colwidth="129"><p>引用</p></td></tr></tbody>

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>
  #22 = Utf8               (I)V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               getAge
  #26 = Utf8               ()I
  #27 = Utf8               main
  #28 = Utf8               ([Ljava/lang/String;)V
  #29 = Utf8               SourceFile
  #30 = Utf8               ByteCodeDemo.java
  #31 = NameAndType        #21:#47        // "<init>":()V
  #32 = NameAndType        #19:#20        // age:I
  #33 = Utf8               java8/ByteCodeDemo
  #34 = NameAndType        #21:#22        // "<init>":(I)V
  #35 = Class              #48            // java/lang/System
  #36 = NameAndType        #49:#50        // out:Ljava/io/PrintStream;
  #37 = Utf8               java/lang/StringBuilder
  #38 = Utf8               name:xiaomingage:
  #39 = NameAndType        #51:#52        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #40 = NameAndType        #25:#26        // getAge:()I
  #41 = NameAndType        #51:#53        // append:(I)Ljava/lang/StringBuilder;
  #42 = NameAndType        #54:#55        // toString:()Ljava/lang/String;
  #43 = Class              #56            // java/io/PrintStream
  #44 = NameAndType        #57:#58        // println:(Ljava/lang/String;)V
  #45 = Utf8               java/lang/Object
  #46 = Utf8               xiaoming
  #47 = Utf8               ()V
  #48 = Utf8               java/lang/System
  #49 = Utf8               out
  #50 = Utf8               Ljava/io/PrintStream;
  #51 = Utf8               append
  #52 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #53 = Utf8               (I)Ljava/lang/StringBuilder;
  #54 = Utf8               toString
  #55 = Utf8               ()Ljava/lang/String;
  #56 = Utf8               java/io/PrintStream
  #57 = Utf8               println
  #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_Longinfo                                   
  • 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/DLevi...

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
}


思考:這裏看懂了以後,你們能夠本身嘗試下本身寫個稍微複雜的字節碼,而後進行理解,加深一下映像。

最後

下一篇預告,下一篇將會給你們詳細講解如何經過asm去操做字節碼,以及如何去實現咱們上面的功能,喜歡這一系列能夠關注公衆號,不丟失文章

若是你們以爲這篇文章對你有幫助,或者想提早獲取後續章節文章,或者你有什麼疑問想提供1v1免費vip服務,均可以關注個人公衆號:
相關文章
相關標籤/搜索