1.編譯后的方法區,其中存儲的代碼都是一些字節碼指令
2.Java虛擬機執行模型:
java代碼是在一個線程內部執行,每個線程都有自己的執行棧,棧由幀組成,每個幀表示一個方法的調用,每調用一個方法,都將將新的幀壓入執行棧,方法返回時(不管是整成return還是異常返回),該方法對應的幀都將出棧,即按照先進后出的規則。當每個frame創建的時候,其操作棧是空的,符號表由this對象和函數參數組成。
執行棧與操作數棧不一樣,操作數棧包含在執行棧中。每一幀包括局部變量和操作數棧兩部分,操作數棧中包括字節碼指令用來當操作數的值。比如a.equals(b)將創建一幀,此時該幀將有一個空棧,並且a和b作為局部變量
字節碼指令:
由標識該指令的操作碼和固定數目的參數組成,操作碼指定要進行哪一類操作,參數指定具體精確行為。指令分為兩類,一類在局部變量和操作數棧之間傳值,一類從操作數棧彈出值計算后再壓入
例如:
ILOAD,LLOAD,FLOAD,DLOAD,ALOAD讀取一個局部變量,並將其值壓入操作數棧中,其對應的參數是其讀取的局部變量索引i(因為局部變量就是通過索引來進行隨機訪問的),LLOAD和DLOAD加載時需要兩個槽(slot),因為局部變量部分和操作數占部分的每個槽(slot)都可以保存除了long和double之外的java值(long和double需要兩個槽)。
ILOAD:加載boolean、char、byte、short、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
通過一個例子來進行學習,比如以下方法:
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()
那么此時在堆中為該class1的對象a分配了內存空間,此時地址存在棧中,那么此時操作數棧頂就是兩個相同的地址空間,通常在調用對象調用其類中方法前肯定要調用其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