1.從字節碼的角度分析i++
/**
* 從字節碼角度分析 a++ 相關題目
*/
public class Demo3_2 {public static void main(String[] args) {int a = 10;int b = a++ + ++a + a--;System.out.println(a);System.out.println(b);}
}
a++
和 ++a
實際上代表了兩個不同的操作,它們分別對應自增和取數的過程。首先需要明確的是,自增操作是在局部變量上完成的。接著,了解這兩者的區別至關重要:哪一個是先取數據,哪一個是先進行自增(或自減)。
具體來說:
a++
和a--
先取數,再進行局部自增(或自減)。++a
和--a
先進行局部自增(或自減),然后再取數。
因此,上述的代碼最終結果是 b = 34
。
2.條件判斷指令
2.1 幾點說明:
- byte,short,char 都會按 int 比較,因為操作數棧都是 4 字節
- goto 用來進行跳轉到指定行號的字節碼
2.2 案例
public class Demo3_3 {public static void main(String[] args) {int a = 0;if(a == 0) {a = 10;} else {a = 20;}
}//字節碼
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12 //判斷是否為0,若是跳到12行
6: bipush 10
8: istore_1
9: goto 15 //跳轉
12: bipush 20
14: istore_1
15: return
3. 循環控制指令
好的,下面是經過整理和潤色后的內容:
在 Java 中,循環控制結構其實是通過之前介紹的字節碼指令來實現的。我們通過分析 while
、do while
和 for
循環的字節碼,可以更好地理解它們背后的實現原理。
3.1. while
循環字節碼示例
對于以下的 while
循環代碼:
public class Demo3_4 {public static void main(String[] args) {int a = 0;while (a < 10) {a++;}}
}
對應的字節碼是:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
字節碼的執行流程如下:
- 初始化變量
a
(即istore_1
)。 - 每次循環時,檢查
a
是否小于 10(通過if_icmpge
判斷)。 - 如果條件為
true
,則執行iinc
(自增a
),然后跳回循環判斷(goto 2
)。 - 如果條件為
false
,則退出循環并結束程序。
3.2 do while
循環字節碼示例
對于以下的 do while
循環代碼:
public class Demo3_5 {public static void main(String[] args) {int a = 0;do {a++;} while (a < 10);}
}
對應的字節碼是:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return
字節碼的執行流程如下:
- 初始化變量
a
(istore_1
)。 - 在循環內部先執行
iinc
(自增a
),然后檢查a
是否小于 10。 - 如果
a
小于 10,跳回到步驟 2,繼續自增并檢查條件。 - 如果條件不滿足,則退出循環并結束程序。
3.3 for
循環字節碼示例
最后是 for
循環。其實,如果我們仔細觀察 while
和 for
循環的字節碼,我們會發現它們是一模一樣的。盡管語法上 for
循環包括了初始化、條件判斷和自增部分,但在字節碼層面,它們表現得幾乎是相同的。
因此,以下是 for
循環的字節碼:
public class Demo3_6 {public static void main(String[] args) {for (int a = 0; a < 10; a++) {// do something}}
}
字節碼也是與 while
循環的字節碼完全相同:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
4.判斷的結果
請從字節碼角度分析,下列代碼運行的結果:
public class Demo3_6_1 {public static void main(String[] args) {int i = 0;int x = 0;while (i < 10) {x = x++;i++;}System.out.println(x); // 結果是 0}
}
分析結果為什么是0,是因為x++,先取數后自增,取數到操作棧依舊是0,局部變量位置是1,但是x=,這個表示復制,又把局部變量的位置改成0,這樣就陷入死循環,無論循環多少次一直為0.
5.構造方法
5.1 <cint> ()v
public class StaticTest {static int i = 10;static {i = 30;}static {i = 20;}public static void main(String[] args) {}
}
編譯器會按從上至下的順序,收集所有靜態代碼塊和靜態成員賦值的代碼,合并為一個特殊的方法
Code:0: bipush 102: putstatic #7 // Field i:I5: bipush 307: putstatic #7 // Field i:I10: bipush 2012: putstatic #7 // Field i:I15: return
5.2 <int>()V
public class Demo3_8_2 {private String a = "s1";{b = 20;}private int b = 10;{a = "s2";}public Demo3_8_2(String a, int b) {this.a = a;this.b = b;}public static void main(String[] args) {Demo3_8_2 d = new Demo3_8_2("s3", 30);System.out.println(d.a);System.out.println(d.b);}
}
編譯器會按從至下的順序,收集所有{}代碼和成員變量,形成新的構造方法,但是原始的構造放內的代碼執行總是在最后面
因此這個代碼的結果是:s3和30
6.方法的調用
public class Demo3_9 {
public Demo3_9() { }private void test1() { }private final void test2() { }public void test3() { }public static void test4() { }public static void main(String[] args) {Demo3_9 d = new Demo3_9();d.test1();d.test2();d.test3();d.test4();Demo3_9.test4();}
}
字節碼
0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
幾種類型的方法的調用說明一下:
- invokespecial,普通私有方法的調用
- invokevirtual,調用實例方法,通過對象的類型決定調用哪個方法(普通方法調用)。
- invokestatic,靜態的方法調用
整個流程為:
- new 新建對象,給對象分配堆內存,然后把對象引用壓入操作數棧
- dup,是把對象復制一份,為什么要復制一份呢,一個是調用對象構造方法,一個是把這個對象賦給局部變量
- 然后調用方法就行
- 備注:d.test3();這種形式調用靜態方法,會有一個pop出棧的操作,這樣就會把【對象引用】從操作數彈掉
7.多態是在字節碼中是如何調用的
當執行 invokevirtual 指令時,
- 獲取對象引用:從棧幀中的操作數棧中獲取方法調用的對象引用。
- 分析對象頭:通過對象引用找到對象的
Class
對象,進一步分析該對象的實際類型。 - 查找 vtable:通過對象的實際類型,獲取其
vtable
(虛方法表)。 - 查表得到方法地址:根據方法名和簽名,在
vtable
中查找到對應的方法地址,確保方法是動態綁定的。 - 執行方法字節碼:跳轉到方法的字節碼地址并執行該方法。
8.異常處理
8.1 catch
public class Demo3_11_1 {
public static void main(String[] args) {int i = 0;try {i = 10;} catch (Exception e) {i = 20;}}
}//main方法的字節碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
StackMapTable: ...
MethodParameters: ...
}
- 可以看到多出來一個 Exception table 的結構,[from, to) 是前閉后開的檢測范圍,一旦這個范圍內的字節碼執行出現異常,則通過 type 匹配異常類型,如果一致,進入 target 所指示行號
- 8 行的字節碼指令 astore_2 是將異常對象引用存入局部變量表的 slot 2 位置
8.2 finally
public class Demo3_11_4 {
public static void main(String[] args) {int i = 0;try {i = 10;} catch (Exception e) {i = 20;} finally {i = 30;}}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try --------------------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // finally |
7: istore_1 // 30 -> i |
8: goto 27 // return -----------------------------------
11: astore_2 // catch Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
21: astore_3 // catch any -> slot 3 ----------------------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw ------------------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的異常類型,比如 Error
11 15 21 any // 剩余的異常類型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: ...
MethodParameters: ...
由于finally必須要執行,可以看到 finally 中的代碼被復制了 3 份,分別放入 try 流程,catch 流程以及 catch 剩余的異常類型流程,最后一份是,怕catch捕獲不到的異常,執行不了所有在其他異常也復制了一份
8.2.1finally面試題1
public class Demo3_12_2 {
public static void main(String[] args) {int result = test();System.out.println(result);public static int test() {try {return 10;} finally {return 20;}}
}//方法字節碼
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10 放入棧頂
2: istore_0 // 10 -> slot 0 (從棧頂移除了)
3: bipush 20 // <- 20 放入棧頂
5: ireturn // 返回棧頂 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入棧頂
9: ireturn // 返回棧頂 int(20)
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...
由這個字節碼我們可以看到,finally里面的邏輯一定會執行,但是如果 finally
中有 return
,它會覆蓋任何異常或者其他的返回值。
8.2.2 finally面試題2
public class Demo3_12_2 {public static void main(String[] args) {int result = test();System.out.println(result);}public static int test() {int i = 10;try {return i;} finally {i = 20;}}
}
//字節碼
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入棧頂
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1,暫存至 slot 1,目的是為了固定返回值
5: bipush 20 // <- 20 放入棧頂
7: istore_0 // 20 -> i
8: iload_1 // <- slot 1(10) 載入 slot 1 暫存的值
9: ireturn // 返回棧頂的 int(10)
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
通過使用 istore_1 暫存 i 的值,字節碼確保了即使在 finally 塊中修改了 i 的值(將其設置為 20局部變量
),最終返回的仍然是 try 塊中的原始 i 值(即 10)。
8.2.2 finally面試題2
public class Demo3_12_2 {public static void main(String[] args) {int result = test();System.out.println(result);}public static int test() {int i = 10;try {return i;} finally {i = 20;}}
}
//字節碼
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入棧頂
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1,暫存至 slot 1,目的是為了固定返回值
5: bipush 20 // <- 20 放入棧頂
7: istore_0 // 20 -> i
8: iload_1 // <- slot 1(10) 載入 slot 1 暫存的值
9: ireturn // 返回棧頂的 int(10)
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
istore_1,可以把值固定住
9.synchronnized
public class Demo3_13 {public static void main(String[] args) {Object lock = new Object();synchronized (lock) {System.out.println("ok");}}
}//字節碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V
7: astore_1 // lock引用 -> lock
8: aload_1 // <- lock (synchronized開始)
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: ...
MethodParameters: ...
幾點說明:
- 為什么字節碼第
9
行要把鎖的引用復制兩份,因為是需要給兩個操作,一個加鎖,一個解鎖 - 還有就是異常捕獲,由于對程序進行加鎖,如果在執行程序的過程發生什么異常,導致程序沒有解鎖,這樣就會程序就會出問題。因此要設置異常捕獲,如果出現異常,就還是要進行解鎖,再拋出異常
- 方法級別的 synchronized 不會在字節碼指令中有所體現