Java ASM學習(2)

1.編譯後的方法區,其中存儲的代碼都是一些字節碼指令java

2.Java虛擬機執行模型:面試

java代碼是在一個線程內部執行,每一個線程都有本身的執行棧,棧由幀組成,每一個幀表示一個方法的調用,每調用一個方法,都將將新的幀壓入執行棧,方法返回時(不論是整成return仍是異常返回),該方法對應的幀都將出棧,即按照先進後出的規則。數組

執行棧與操做數棧不同,操做數棧包含在執行棧中。每一幀包括局部變量和操做數棧兩部分,操做數棧中包括字節碼指令用來當操做數的值。好比a.equals(b)將建立一幀,此時該幀將有一個空棧,而且a和b做爲局部變量函數

字節碼指令:學習

由標識該指令的操做碼和固定數目的參數組成,操做碼指定要進行哪一類操做,參數指定具體精確行爲。指令分爲兩類,一類在局部變量和操做數棧之間傳值,一類從操做數棧彈出值計算後再壓入this

例如:spa

ILOAD,LLOAD,FLOAD,DLOAD,ALOAD讀取一個局部變量,並將其值壓入操做數棧中,其對應的參數是其讀取的局部變量索引i(由於局部變量就是經過索引來進行隨機訪問的),LLOAD和DLOAD加載時須要兩個槽(slot),由於局部變量部分和操做數佔部分的每一個槽(slot)均可以保存除了long和double以外的java值(long和double須要兩個槽)。線程

ILOAD:加載boolean、charbyteshort、int局部變量
LLOAD:加載long
FLOAD:加載float
DLOAD:加載double
ALOAD:加載對象和數組引用

對應的ISTORE,LSTORE,FSTORE,DSTORE,ASTORE從操做數棧彈出值並將其存儲在指定的索引i所表明的局部變量中,因此這些操做指令是和java數據類型密切相關的。存取值和數據類型也相關,好比使用ISTORE 1 ALOAD 1,此時從操做數棧彈出一個int值存入索引1處的局部變量中,再將該值轉爲對象類型進行轉換讀取是非法的。可是對於一個局部變量位置,咱們能夠在運行過程當中改變其類型,好比ISTORE 1 ALOAD 1非法,可是ATORE 1 ALOAD1就合法了。具體的字節碼指令見ASM指南附A.1設計

經過一個例子來進行學習,好比如下方法:3d

package asm;

public class bean {
    private int f;

    public bean() {
    }

    public void setF(int f) {
        this.f = f;
    }

    public int getF() {
        return this.f;
    }
}

直接經過字節碼文件查看其class文件結構,其字段就一個int類型的f,訪問修飾符爲private

setf方法的字節碼指令以下

 其局部變量表以下,因此有兩個值一個就是當前對象this和成員變量f,分別對應下標0和1

 這裏要設計到幾個字節碼指令:

GETFIELD owner name desc:讀取一個字段的值並將其值壓入操做數棧中
PUTFIELD owner name desc:從操做數彈出值存在name所表明的字段中
owner:類的全限定名
GETSTATIC owner name desc和PUTSTATIC owner name desc相似,只是爲靜態變量

aload 0,讀取局部變量this,也就是局部變量表下標爲0處的this對象(其在調用這個方法的時候就已經初始化存儲在局部變量表中),而後將其壓入操做數棧。

iload 1,讀取局部變量f,下標爲1(建立幀期間已經初始化,也就是入口參數int f),壓入操做數棧中

putfield #2 <asm/bean.f> 也就是彈出壓入的兩個值,賦值給asm/bean.f,也就是將入口的int f的值賦給this.f

return 即該方法執行完成,那麼該幀從執行棧從彈出

getf對應的字節碼指令以下所示:

aload 0,即從局部變量表拿到this放入操做數棧

getfield #2 <asm/bean.f> 即從操做數棧中拿出this,並將this.f的值壓入操做數棧

ireturn 返回f的值get方法的調用者,xreturn,x即返回變量對應的修飾符

bean構造方法,字節碼指令以下:

aload 0: 從局部變量表拿到this,壓入操做數棧

這裏要設計方法的調用相關的字節碼指令:

INVOKEVIRTUAL owner name desc:
調用owner所表示的類的name方法
desc用來描述一個方法的參數類型和返回類型 INVOKESTATIC:調用靜態方法 INVOKESPECIAL: 調用私有方法和構造器 INVOKEINTERFACE: 接口中定義的方法

invokespecial #1 <java/lang/Object.<init>>: 調用object對象的init方法,即super()調用,最後return返回,若是是對於如下代碼:

package asm;

public class bean {
    private int f;

    public void setFf(int f) {
        if(f>0){
        this.f = f;}
        else {
            throw new IllegalArgumentException();
        }
    }

    public int getF() {
        return f;
    }

}

此時setf的字節碼指令以下:

iload  1,從局部表量表中拿出入口參數 int f,壓入操做數棧

ifile 9:此時彈出操做數棧中的int f和0進行比較

a.若是小於等於0(這裏將大於判斷轉爲小於等於的判斷),則到第12條指令 

new #2 :新建一個異常對象並壓入操做數棧

dup:重複壓入該值一次

invokespecial #4  : 彈出操做棧中兩個對象值其中之一,並調用其構造函數實例化該對象

athrow:彈出操做數棧中剩下的值(另外一個異常對象),並將其做爲異常拋出

b.若是大於0,則依次執行

aload0 從局部變量表拿出this對象放入操做數棧中

iload1 拿出入口int f的值壓入棧中

putfiled #2 <asm/bean.f>:將int f的值賦給this.f

goto 20: 到第20條字節碼指令

return : 返回

感受和彙編有點像,不過比彙編更容易理解,主要仍是方法內的一些操做,能看懂基本的字節碼指令,複雜的再去查doc,據說面試有時候會問i++和++i的區別:

package asm;

public class testplus {

    public void plusf(){
        int i=0;
        System.out.println(i++);
    }
       public void pluse(){
        int i=0 ;
        System.out.println(++i);
       }
}

編譯後:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asm;

public class testplus {
    public testplus() {
    }
  //i++
    public void plusf() {
        int i = 0;
        byte var10001 = i;
        int var2 = i + 1;
        System.out.println(var10001);
    }
  //++i
    public void pluse() {
        int i = 0;
        int i = i + 1;
        System.out.println(i);
    }
}

首先從生成的class來看,i++編譯後居然用字節存儲了i的值,而後i自增1,輸出的爲字節類型i即0,因此i++,最終輸出爲0,++i,直接是i自增1,而後輸出i,因此最終輸出爲1因此for循環用i++,而不用++i

從字節碼指令來看:

i++

iconst 0:首先操做數棧中壓入常量0

istore 1:而後彈出常量0放入局部變量表索引1處,此時局部變量表處1處從i變爲0,操做數棧空

getstatic #2 :即拿到java.lang.System.out,即取靜態變量System.out壓入棧中,此時棧中1元素

 

#2在常量池中爲第二個,關於該字段的引用說明以下,out對應的描述符即爲Ljava/io/PrintStream; 那麼類類型的描述符就是L+類的全限定名+;

 

iload 1:從局部變量表1處取值,壓住操做數棧,即將0壓入操做數棧

iinc 1 by 1:給局部變量1處的值+1,此時1處即從0變爲1

invokevirtual:調用java.io.PrintStream.println,此時須要的值是從操做數棧中取的,然而此時操做數棧頂彈出的數值爲0,因此輸出爲0

++i

 

iconst 0:首先操做數棧中壓入常量0

istore 1:而後彈出常量0放入局部變量表索引1處,此時局部變量表處1處從i變爲0,操做數棧空

getstatic #2 :即拿到java.lang.System.out,即取靜態變量System.out壓入棧中,此時棧中1元素

iinc 1 by 1:將局部變量表1處的值加1,即從0變爲1

iload 1:加載局部變量表1處的值,壓入操做數棧中,即將1壓入棧中

invokevirtual:調用java.io.PrintStream.println,此時須要的值是從操做數棧中取的,然而此時操做數棧頂彈出的數值爲1,因此輸出爲1

因此i++和++i的區別從字節碼指令上來看就是局部變量表自增和壓入操做數棧的順序不同,i++是先壓棧,後局部變量表自增,++i是先局部變量表自增,後壓入操做數棧,這樣就徹底搞懂了2333~

因此再分析一個鞏固鞏固:

package asm;

public class testplus {

       public void pluse(){
        int i=0 ;
        int p = 2 + i++ - ++i;
        System.out.println(i);
        System.out.println(p);
       }

    public static void main(String[] args) {
        testplus t = new testplus();
        t.pluse();
    }
}

main方法:

new #4 <asm/testplus>:new一個對象壓入棧中

dup:賦值一個棧頂的對象再壓入操做數棧,關於爲何要壓入兩個重複的值緣由:

首先字節碼指令操做數值時基於棧實現的,那麼對於同一個值從棧中操做時一定要彈出,那麼若是對一個數同時操做兩次,那麼就要兩次壓棧。涉及到new一個對象操做時,java虛擬機自動dup,在new一個對象之後,棧中放入的是該對象在堆中的地址,好比聲明如下兩個

class1 a = new class1();
a.pp()

一般在調用對象調用其類中方法前確定要調用其init實例化,那麼init要用一次操做數棧中的地址,此時彈出一次地址參與方法調用,後面只須要再將該棧中的地址放入局部變量表,該地址的對象已經完成了實例化操做,那麼後面每次調用只須要從局部變量表從取到該對象的地址,便可任意調用其類中的方法。

invokespecial #5 :這裏調用testplus的init方法,因此從棧中彈出一個testplus的地址

astore 1:將實例化之後的該testplus對象地址放入局部變量表1處

aload 1:取局部變量表1處的對象地址壓入棧中

invokevirtual #6:調用testplus的pluse方法

return :返回

pluse方法:

 

iconst 0:壓入常量0

istore 1:彈出0存入局部變量表1處 (完成int i=0)

iconst 2:將2壓入棧中

iload 1:取出局部變量表1處的值0壓入棧中

iinc 1 by 1:局部變量表1處的值加1,即從0變爲1

iadd :將棧中的兩個值相加,即 stack[0] + stack[1] = 2 + 0 =2

iinc 1 by 1: 局部變量表1處的值加1,即從1變爲2

iload 1:去局部變量表1處的值壓入棧中,即棧頂爲2

isub :將棧中兩個元素相減,即stack[0] - stack[1] =  2 - 2 =0

istore 2:彈出棧中的惟一一個元素2,存入局部變量表2處,此時棧空

getstatic # 2 :拿到Syetem.out,壓入棧中

iload 1:取出局部表量表1處的值壓入棧中,即棧頂爲2

invokevirtual  #3 : 彈出棧中兩個元素,調用System.out的println方法,即stack[0].print(stack[1]),即輸出2

同理壓入System.out,而後iload 2,取出局部變量表2處的0壓入棧中,輸出0

最終輸出結果也是2和0

相關文章
相關標籤/搜索