前言簡介
前文已經對虛擬機進行過了簡單的介紹,並且也對class文件結構,以及字節碼指令進行了詳盡的說明
想要了解JVM的運行機制,以及如何優化你的代碼,你還需要了解一下,java編譯器到底是如何編譯你的代碼的
本文不是從最底層的編譯原理講解
本文是針對java代碼,去查看歸納總結編譯器的結果行為,從而直觀的感受到字節碼指令集
也就是說本文的內容,主要針對的是使用javap 查看字節碼文件中方法的code屬性中的字節碼內容
讓你從java代碼 class文件格式,以及字節碼指令集 進行一個直觀的演示
提醒:
如果你對字節碼指令不了解,而且,沒有看過前面的文章,本文可能會輕度不適.
本文示例只是為了展示
您應該經常查看你自己的代碼的class文件去發現其中的規律
一條普通的指令格式
<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]
index 表示偏移量 行號 等
opcode 表示操作碼
operandX表示操作數
comment 為注釋
|
比如下圖所示
行號 0 , 操作碼 getstatic ,操作數 #24 注釋為 Field java/lang/System..................
其中 index 行號/偏移量 可以作為控制跳轉指令的跳轉目標 比如 goto 8 表示跳轉到索引為8的指令上 |
還有一點需要注意的是,javap查看到的內容,你可以認為是class文件表述的信息,但是絕不能理解為就是class文件中的內容
比如,class文件中沒有操作碼的助記符,比如,getstatic ,都是指令的二進制值
再比如剛才說到的,跳轉到指定行號,對於控制轉移指令,實際的操作數是在當前指令的操作碼集合中的地址偏移量
並不是那個8
只不過javap工具按照更適合我們閱讀的方式進行了翻譯
加載存儲與算數指令
public static void main(String[] args) { int i = -1; int j = 3; int k = 5; int l = 127; int m = 32767; int n = 32768; int add = i+j; int sub = i-j; int mul = j*k; int div = j/k; int rem = k%j; int neg = ~j; int inc = i++; }
類型轉換指令
public static void main(String[] args) { boolean bNum = true; char cNum = 2; byte byteNum = 127; short sNum = 32767; int iNum = 100; long lNum = 65536; float fNum = 2.5f; double dNum = 6.8;
char c1 = (char)byteNum; char c2 = (char)sNum; char c3 = (char)iNum; char c4 = (char)lNum; char c5 = (char)fNum; char c6 = (char)dNum; byte b1 = (byte)cNum; byte b2 = (byte)sNum; byte b3 = (byte)iNum; byte b4 = (byte)lNum; byte b5 = (byte)fNum; byte b6 = (byte)dNum; short s1 = (short)cNum; short s2 = (short)byteNum; short s3 = (short)iNum; short s4 = (short)lNum; short s5 = (short)fNum; short s6 = (short)dNum; int i1 = (int)cNum; int i2 = (int)byteNum; int i3 = (int)sNum; int i4 = (int)lNum; int i5 = (int)fNum; int i6 = (int)dNum; long l1 = (long)byteNum; long l2 = (long)cNum; long l3 = (long)sNum; long l4 = (long)iNum; long l5 = (long)fNum; long l6 = (long)dNum; float f1 = (float)byteNum; float f2 = (float)cNum; float f3 = (float)sNum; float f4 = (float)iNum; float f5 = (float)lNum; float f6 = (float)dNum; double d1 = (double)byteNum; double d2 = (double)cNum; double d3 = (double)sNum; double d4 = (double)iNum; double d5 = (double)lNum; double d6 = (double)fNum; }
javap解析后的內容太長,接下來分段解析
數據的加載與存儲
從數據的存儲可以看得出來 boolean內部使用的是數值1 也就是1 表示true
數據類型轉換為char類型
char byte short int 內部形式均為int 所以轉換為char是,使用的全都是 i2c
long float double 先轉換為int(l2i f2i d2i) 然后在統一使用 i2c 轉換為char
數據類型轉換為byte 類型
char byte short int 內部形式均為int 所以轉換為byte時,使用的全都是 i2b
long float double 先轉換為int(l2i f2i d2i) 然后在統一使用 i2b 轉換為 byte
數據類型轉換為short 類型
還是同樣的道理,
char byte short int 內部形式均為int 所以轉換為short 使用的是 i2s
long float double 先轉換為int(l2i f2i d2i) 然后在統一使用 i2s 轉換為 short
數據類型轉換為int 類型
char byte short內部都是int類型.將他們轉換為int時,不需要進行轉換
如下圖所示,一個load 對應一個store
long float double (l2i f2i d2i) 轉換為int
數據類型轉換為long 類型
char byte short int 內部都是int類型.將他們轉換為long 時,使用 i2l
float double 轉換為long f2l d2l
數據類型轉換為float 類型
char byte short int 內部都是int類型.將他們轉換為float 時,使用 i2f
long double 轉換為float l2f d2f
數據類型轉換為double 類型
char byte short int 內部都是int類型.將他們轉換為double 時,使用 i2d
long
float 轉換為double l2d f2d
類相關指令
class Super{ } class Sub extends Super{ } new Object(); new Super(); Super s = new Super(); new Double(1.5); new Sub(); Sub sub = new Sub();
new Object();
new Super();
沒有賦值給局部變量 僅僅是創建對象 調用new之后,堆中對象的引用保存在棧頂
然后調用構造方法invokespecial
Super s = new Super();
同上面的,需要調用new
因為還需要保存到局部變量
所以new之后 先copy一個,也就是dup
然后調用構造方法 invokespecial
然后從操作數棧保存到局部變量 store
|
Super super1 = new Super(); Sub sub = new Sub(); //父類引用可以指向子類 //子類引用不能指向父類 //但是對於指向子類的父類引用 可以通過類型轉換為子類 Super subToSuper = sub; Sub superToSub = (Sub) subToSuper;
0 創建Spper 3 復制 4 調用構造方法 7 保存到1號局部變量 8 創建Sub 11 復制 12調用構造方法 15 保存到2號局部變量 16 2號加載到操作數棧 17保存到3號局部變量 18加載3號局部變量到棧 19 checkcast 進行校驗確認是否可以轉換為指定類型 否則報錯拋 classCastException 22 再次保存到局部變量 |
控制轉移指令
void intWhile() { int i = 0; while (i < 100) { i++; } } void intDoWhile() { int i = 0; do { i++; } while (i < 100); } void intFor() { int j = 0; for(int i =0;i<100;i++) { j++; } }
intWhile()方法 0. 加載常量0 到操作數棧 1.保存操作數棧元素到1號局部變量 i= 0; 2.直接跳轉到第8行 8.1號局部變量加載到操作數棧 也就是i 作為第一個元素 9.加載常量100到操作數棧 也就是100作為第二個元素 11.比較大小,如果前者小於后者 也就是如果 i <100 滿足 跳轉到第5行 否則順序執行到14 return 5.給1號局部變量以增量1 增加 然后 8-->9-->11-->5-->8-->9-->11......往復循環 直到條件不滿足,從11 跳轉到14 結束 |
intDoWhile() 0.加載常量0到操作數棧 1.保存常量0 到1號局部變量 2.給1號局部變量以增量1 進行自增 5.1號局部變量加載到操作數棧 6.常量100加載到操作數棧 8,比較大小 如果前者小於后者也就是 1號局部變量 i<100 跳轉到第2行 然后進行往復循環,直到條件不滿足,然后順序到return |
intFor() 0. 加載常量0 到操作數棧 1. 保存棧頂元素到1號局部變量 j=0; 2. 加載常量0到操作數棧 3. 保存棧頂元素到2號局部變量i=0; 4. 跳轉到13行 13. 加載2號局部變量到操作數棧 14. 加載常量100到操作數棧 16. 比較大小,如果前者 2號局部變量 i <100 跳轉到7 7. 1號局部變量以增量1 自增 j++ 10. 2號局部變量以增量1 自增 i++ 13. 2號局部變量加載到操作數棧
14. 加載常量100到操作數棧
16. 比較大小,如果前者 2號局部變量 i <100 跳轉到7
往復循環 如果條件不滿足 從16 順序到19 結束方法 return
|
public void fun() { int i = 0; if(i<2) { i++; }else { i--; } }
0, 加載常量0 到棧頂 1,保存棧頂元素 (0) 到1號局部變量 2. 加載1號局部變量到棧頂 3. 加載常量2 到棧頂 4,比較 如果大於后者等於跳轉到13 然后1號局部變量 自增1 然后下一步順序到16 return 否則就是順序執行到7 1號局部變量 增量為-1 自增運算 然后到10 ,10為跳轉到16 return |
方法調用相關指令
public void invoker() { method(2); } public void method(int i) { if(i>5) { System.out.println(i); } }
invoker() 0,加載0號 局變量到棧 (上面基本都是第一個數據被保存到1號局部變量,0 號其實是被this 占用了) 1,加載常量2 到操作數棧 2.調用實例方法(I)V 5 return method(int) 0. 加載1號局部變量到操作數棧 1. 加載常量5 到操作數棧 2比較如果小於等於 跳轉到12行 直接返回 如果大於 那么順序執行到5行 out 是類型為PrintStream的 System中的靜態變量 8 加載1號局部變量到操作數棧 9 調用實例方法 println 是 PrintStream的實例方法 使用invokevirtual |
switch 相關
int i = 5; int j = 6; switch (i) { case 1: j = j + 1; break; case 3: j = j + 2; break; case 5: j = j + 3; break; default: j = j + 4; }
0,1,2,4 分別將 5 和 6 加載並存儲到1號和2號局部變量 5.加載1號局部變量到棧 對應 switch (i) { 然后根據tableswitch 表 進行跳轉 雖然我們只有1,3,5 但是設置了1到5 ,對於2 和 4 直接跳轉到default 40: 2號局部變量 +1 順序到43 43: 跳轉到61 return
46: 2號局部變量 +2
順序到49
49: 跳轉到61 return
52: 2號局部變量 +3
順序到55
55: 跳轉到61 return
58 2號局部變量 +4 順序到61 return |
int j = 6; String string = "hehe"; switch (string) { case "A": j = j + 1; break; case "hehe": j = j + 2; break; case "C": j = j + 3; break; default: j = j + 4; }
代碼千千萬,本文只是找一些基本的示例展示字節碼與代碼的對應關系,想要熟悉這塊
唯有沒事多javap看看你代碼的class文件,才能通宵領悟,進而更好地優化你的代碼
比如看看下面的一個很典型的例子
int i = 5; int j = 8; int k = i+j; int l = 3+6;
前一部分: 0. 常量5 加載到棧 1,保存到 1號局部變量 2. 常量8 加載到棧 4 保存到2號 局部變量 5,加載1號局部變量 6, 加載2號局部變量 7 執行iadd 結果會壓入棧頂 8 棧頂元素保存到3號局部變量 至此 完成了前三行代碼 后一部分: 9.常量9 加載到棧 (3+6 已經被計算好了) 11,保存到4號局部變量 |