做建筑設計網站怎么建立網站平臺
文章目錄
- 前言
- 1. if語句
- 字節(jié)碼的解析
- 2. for循環(huán)
- 字節(jié)碼的解析
- 3. while循環(huán)
- 4. switch語句
- 5. try-catch語句
- 6. i++ 和++i的字節(jié)碼
- 7. try-catch-finally
- 8. 參考文檔
前言
上一章我們聊了《JVM字節(jié)碼指令詳解》 。本章我們學以致用,聊一下我們常見的一些java語句的特性底層是如何實現。
1. if語句
if語句是我們最常用的判斷語句之一,它的底層實現原理是什么呢?可以通過反編譯字節(jié)碼來分析一下。
假設我們有以下的java代碼:
public class IfStatement {public static void main(String[] args) {int a = 10;if (a > 0) {System.out.println("a is positive");} else {System.out.println("a is negative or zero");}}
}
可以使用javap命令來反編譯字節(jié)碼:
javap -c IfStatement.class
輸出結果如下:
public class IfStatement {// 構造方法public IfStatement();Code:0: aload_0 // 將局部變量表中的第0個元素(通常是this引用)入棧1: invokespecial #1 // 調用父類Object的構造方法4: return // 方法返回// main方法public static void main(java.lang.String[]);Code:0: bipush 10 // 將10壓入棧頂2: istore_1 // 將棧頂元素(10)存入局部變量表的第1個位置3: iload_1 // 將局部變量表的第1個位置的元素(10)入棧4: ifle 17 // 判斷棧頂元素(10)是否小于等于0,如果是則跳轉到指令177: getstatic #2 // 獲取System類的out字段,類型是PrintStream10: ldc #3 // 將字符串"a is positive"壓入棧頂12: invokevirtual #4 // 調用PrintStream的println方法輸出字符串15: goto 23 // 無條件跳轉到指令2318: getstatic #2 // 獲取System類的out字段,類型是PrintStream21: invokevirtual #5 // 調用PrintStream的println方法輸出局部變量表第1個位置的元素(10)24: return // 方法返回
}
字節(jié)碼的解析
- 在main方法中,首先將10壓入棧頂,然后將其存入局部變量表的第1個位置。然后將局部變量表的第1個位置的元素(10)入棧,判斷其是否小于等于0,如果是則跳轉到指令17,否則執(zhí)行下一條指令。在指令7-15中,它獲取System的out字段,將字符串"a is positive"壓入棧頂,然后調用println方法輸出這個字符串,最后無條件跳轉到指令23。在指令18-21中,它獲取System的out字段,然后調用println方法輸出局部變量表第1個位置的元素(10)。最后,main方法返回。
2. for循環(huán)
for循環(huán)是我們常用的循環(huán)語句之一,它的底層實現原理是什么呢?我們還是可以通過反編譯字節(jié)碼來分析一下。
假設我們有以下的java代碼:
public class ForLoop {public static void main(String[] args) {for (int i = 0; i < 10; i++) {System.out.println("i = " + i);}}
}
可以使用javap命令來反編譯字節(jié)碼:
javap -c ForLoop.class
這是一個包含for循環(huán)的Java類ForLoop的字節(jié)碼輸出,下面是中文注釋:
public class ForLoop {// 構造方法public ForLoop();Code:0: aload_0 // 將局部變量表中的第0個元素(通常是this引用)入棧1: invokespecial #1 // 調用父類Object的構造方法4: return // 方法返回// main方法public static void main(java.lang.String[]);Code:0: iconst_0 // 將0壓入棧頂1: istore_1 // 將棧頂元素(0)存入局部變量表的第1個位置2: iload_1 // 將局部變量表的第1個位置的元素(0)入棧3: bipush 10 // 將10壓入棧頂5: if_icmpge 19 // 如果局部變量表的第1個位置的元素(0)大于等于棧頂元素(10),則跳轉到指令198: getstatic #2 // 獲取System類的out字段,類型是PrintStream11: new #3 // 創(chuàng)建一個StringBuilder類的對象14: dup // 復制棧頂元素,此時棧頂有兩個相同的StringBuilder對象引用15: invokespecial #4 // 調用StringBuilder類的構造函數初始化對象18: ldc #5 // 將字符串"i ="壓入棧頂20: invokevirtual #6 // 調用StringBuilder的append方法將字符串添加到StringBuilder23: iload_1 // 將局部變量表的第1個位置的元素(0)入棧24: invokevirtual #7 // 調用StringBuilder的append方法將數字添加到StringBuilder27: invokevirtual #8 // 調用StringBuilder的toString方法將StringBuilder轉化為字符串30: invokevirtual #9 // 調用PrintStream的println方法輸出字符串33: iinc 1, 1 // 將局部變量表的第1個位置的元素(0)增加136: goto 2 // 無條件跳轉到指令2,形成循環(huán)39: return // 方法返回LineNumberTable:line 3: 0line 4: 8line 3: 33line 6: 39StackMapTable: number_of_entries = 2frame_type = 252 /* append */offset_delta = 2locals = [ int, int ]stack = []frame_type = 250 /* chop */offset_delta = 36
}
字節(jié)碼的解析
可以看到,for循環(huán)的底層實現是通過if_icmpge指令來實現的。在本例中,當i小于10時,會執(zhí)行第8行的輸出語句;否則,會跳轉到第39行,結束循環(huán)。
- 在main方法中,首先將0壓入棧頂,然后將其存入局部變量表的第1個位置。接下來是一個循環(huán),循環(huán)條件是局部變量表的第1個位置的元素小于10。在循環(huán)體中,它首先獲取System的out字段,然后創(chuàng)建一個StringBuilder對象并初始化,然后將字符串"i ="和局部變量表的第1個位置的元素添加到StringBuilder,然后將StringBuilder轉化為字符串,然后調用println方法輸出字符串。在循環(huán)體結束時,它將局部變量表的第1個位置的元素加1,然后無條件跳轉到指令2,形成循環(huán)。當循環(huán)結束時,main方法返回。
3. while循環(huán)
while循環(huán)是我們常用的循環(huán)語句之一,它的底層實現原理是什么呢?我們還是可以通過反編譯字節(jié)碼來分析一下。
假設我們有以下的java代碼:
public class WhileLoop {public static void main(String[] args) {int i = 0;while (i < 10) {System.out.println("i = " + i);i++;}}
}
可以使用javap命令來反編譯字節(jié)碼:
javap -c WhileLoop.class
輸出結果如下:
public class WhileLoop {public WhileLoop();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: iconst_01: istore_12: iload_13: bipush 105: if_icmpge 198: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: new #3 // class java/lang/StringBuilder14: dup15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V18: ldc #5 // String i =20: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;23: iload_124: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;27: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;30: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V33: iinc 1, 136: goto 239: returnLineNumberTable:line 3: 0line 4: 2line 5: 8line 6: 33line 5: 36line 8: 39StackMapTable: number_of_entries = 2frame_type = 252 /* append */offset_delta = 2locals = [ int, int ]stack = []frame_type = 250 /* chop */offset_delta = 36
}
可以看到,while循環(huán)的底層實現也是通過if_icmpge指令來實現的。在本例中,當i小于10時,會執(zhí)行第8行的輸出語句;否則,會跳轉到第39行,結束循環(huán)。
4. switch語句
switch語句是我們常用的分支語句之一,它的底層實現原理是什么呢?我們還是可以通過反編譯字節(jié)碼來分析一下。
假設我們有以下的java代碼:
public class SwitchStatement {public static void main(String[] args) {int i = 2;switch (i) {case 1:System.out.println("i is 1");break;case 2:System.out.println("i is 2");break;case 3:System.out.println("i is 3");break;default:System.out.println("i is neither 1, 2 nor 3");break;}}
}
可以使用javap命令來反編譯字節(jié)碼:
javap -c SwitchStatement.class
輸出結果如下:
public class SwitchStatement {public SwitchStatement();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: iconst_21: istore_12: iload_13: tableswitch { // 1 to 31: 282: 403: 52default: 64}28: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;31: ldc #3 // String i is 133: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V36: goto 7140: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;43: ldc #5 // String i is 245: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V48: goto 7152: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;55: ldc #6 // String i is 357: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V60: goto 7164: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;67: invokevirtual #7 // Method java/io/PrintStream.println:()V70: return71: returnLineNumberTable:line 3: 0line 4: 2line 5: 28line 6: 40line 7: 52line 8: 64line 9: 70line 7: 71StackMapTable: number_of_entries = 5frame_type = 252 /* append */offset_delta = 28locals = [ int ]frame_type = 252 /* append */offset_delta = 11locals = [ int ]frame_type = 252 /* append */offset_delta = 12locals = [ int ]frame_type = 252 /* append */offset_delta = 12locals = [ int ]frame_type = 252 /* append */offset_delta = 3locals = [ int ]
可以看到,switch語句的底層實現是通過tableswitch指令來實現的。在本例中,當i等于1時,會執(zhí)行第28行的輸出語句;當i等于2時,會執(zhí)行第40行的輸出語句;當i等于3時,會執(zhí)行第52行的輸出語句;否則,會執(zhí)行第64行的輸出語句。
5. try-catch語句
try-catch語句是我們常用的異常處理語句之一,它的底層實現原理是什么呢?我們還是可以通過反編譯字節(jié)碼來分析一下。
假設我們有以下的java代碼:
public class TryCatchStatement {public static void main(String[] args) {try {int[] arr = new int[3];arr[4] = 5;} catch (ArrayIndexOutOfBoundsException e) {System.out.println("Array index out of bounds!");}}
}
可以使用javap命令來反編譯字節(jié)碼:
javap -c TryCatchStatement.class
輸出結果如下:
public class TryCatchStatement {public TryCatchStatement();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: iconst_01: newarray int3: astore_14: aload_15: iconst_46: iconst_57: iastore8: goto 1911: astore_112: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;15: ldc #3 // String Array index out of bounds!17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V20: returnException table:from to target type0 8 11 Class java/lang/ArrayIndexOutOfBoundsExceptionLineNumberTable:line 3: 0line 4: 4line 5: 11line 6: 12line 7: 20StackMapTable: number_of_entries = 2frame_type = 34 /* same */frame_type = 1 /* same_locals_1_stack_item */stack = [ class java/lang/ArrayIndexOutOfBoundsException ]
}
可以看到,try-catch語句的底層實現是通過異常表來實現的。在本例中,當數組下標越界時,會執(zhí)行第12行的輸出語句;否則,會跳轉到第20行,繼續(xù)執(zhí)行。
6. i++ 和++i的字節(jié)碼
i++
和 ++i
在語義上有些許不同,在字節(jié)碼層面也有所體現。下面是它們的字節(jié)碼層面的解釋:
假設 i
是局部變量表的索引為1的變量。
i++
的偽字節(jié)碼:
iload_1 // 從局部變量表中加載變量 i 到操作數棧頂
iinc 1 by 1 // 將局部變量表中的變量 i 增加1
++i
的偽字節(jié)碼:
iinc 1 by 1 // 將局部變量表中的變量 i 增加1
iload_1 // 從局部變量表中加載變量 i 到操作數棧頂
可以看到,i++
與 ++i
的主要區(qū)別在于加載和增加操作的順序不同。i++
是先將 i
加載到操作數棧頂,然后再增加 i
的值;而 ++i
是先增加 i
的值,然后再將 i
加載到操作數棧頂。這就解釋了 i++
和 ++i
在語義上的不同:i++
是先取值后加1,++i
是先加1后取值。
7. try-catch-finally
在 Java 字節(jié)碼中,try-catch-finally 結構主要通過異常表(Exception Table)來實現。Java 字節(jié)碼并沒有專門的指令來表示 try、catch 或者 finally 塊。相反,它通過在異常表中記錄 try 塊的開始和結束位置、catch 塊的開始位置和要捕獲的異常類型,以實現異常處理的流程。
下面是一個簡單的 try-catch-finally 代碼例子:
void test() {try {System.out.println("try block");throw new Exception();} catch (Exception e) {System.out.println("catch block");} finally {System.out.println("finally block");}
}
對應的字節(jié)碼指令
0: getstatic #2 // 獲取 java/lang/System 類的 out 字段,是 PrintStream 類型
3: ldc #3 // 將常量池中的 "try block" 字符串壓入棧頂
5: invokevirtual #4 // 調用 PrintStream 類的 println 方法輸出字符串
8: new #5 // 創(chuàng)建一個 java/lang/Exception 類的對象
11: dup // 復制棧頂的元素,此時棧頂有兩個相同的異常對象引用
12: invokespecial #6 // 調用 Exception 類的構造函數初始化對象
15: athrow // 拋出棧頂的異常對象
16: astore_1 // 捕獲異常并存入局部變量表的第1個位置
17: getstatic #2 // 獲取 java/lang/System 類的 out 字段,是 PrintStream 類型
20: ldc #7 // 將常量池中的 "catch block" 字符串壓入棧頂
22: invokevirtual #4 // 調用 PrintStream 類的 println 方法輸出字符串
25: jsr 26 // 無條件跳轉到指令26(finally塊的起始位置)
28: goto 34 // 執(zhí)行完finally塊后,跳過 catch 塊剩下的代碼,進入下一個處理流程
31: astore_2 // 捕獲從finally塊拋出的異常并存入局部變量表的第2個位置
32: jsr 26 // 無條件跳轉到指令26(finally塊的起始位置)
35: aload_2 // 從局部變量表的第2個位置加載異常對象至棧頂
36: athrow // 再次拋出該異常對象
37: astore_3 // 捕獲異常并存入局部變量表的第3個位置
38: getstatic #2 // 獲取 java/lang/System 類的 out 字段,是 PrintStream 類型
41: ldc #8 // 將常量池中的 "finally block" 字符串壓入棧頂
43: invokevirtual #4 // 調用 PrintStream 類的 println 方法輸出字符串
46: ret 3 // 返回到 astore_3 指令之后的代碼
這段字節(jié)碼中使用了 jsr
和 ret
指令,這兩個指令主要用于實現 finally
塊的邏輯。jsr
指令會跳轉到 finally
塊的代碼,然后 ret
指令用于返回到 finally
塊之前的代碼繼續(xù)執(zhí)行。
字節(jié)碼的解釋
-
行0-15:這部分對應 try 塊的內容。在這個例子中,它首先通過 getstatic 指令獲取 System.out 對象,然后通過 ldc 指令加載常量 “try block”,最后調用 println 方法輸出這個字符串。然后,它創(chuàng)建一個 Exception 對象并拋出。
-
行16-25:這部分對應 catch 塊的內容。當 try 塊拋出異常時,執(zhí)行流程會跳轉到這部分。在這個例子中,它首先通過 astore 指令將異常對象存儲到局部變量表,然后類似于 try 塊的處理,輸出 “catch block” 字符串。
-
行37-46:這部分對應 finally 塊的內容。無論 try 塊是否拋出異常,這部分代碼總是會被執(zhí)行。在這個例子中,它輸出 “finally block” 字符串。
-
行25-32和行35-36:這部分是對異常處理的一些額外控制。jsr 和 ret 指令用于實現無條件的跳轉,確保 finally 塊總是會被執(zhí)行。
8. 參考文檔
- 張亞 《深入理解JVM字節(jié)碼》
- https://www.jonesjalapat.com/2021/09/11/internal-working-of-java-virtual-machine/