面試官:解釋一下Java位元組碼文件中的JVM指令
- 2019 年 11 月 22 日
- 筆記
Java 之所以流行,一個很重要的原因就是它的跨平台特性,Compile Once, Run Anywhere,編譯一次,到處運行。即 Java 源碼只需要編譯成位元組碼文件,之後就可以在不同的作業系統(Windows、Mac、Linux)運行,準確講是運行在作業系統上的 JVM 中。
我們都知道通過命令 javac 來編譯 Java 源程式碼,但是編譯的具體流程步驟你有沒有深入了解一下呢?相信很多朋友都沒有關注過這一塊的內容,今天楠哥就帶大家一探究竟,梳理一下 Java 的編譯過程。
1、創建一個 Java 源文件 HelloWorld.java,並在 main 方法中完成簡單的邏輯操作,如下所示。
public class HelloWorld { public static void main(String[] args) { int i = 10; int j = 20; int k = i+j; System.out.println(k); } }
2、在終端通過 javac 命令編譯 HelloWorld.java。
javac HelloWorld.java
3、編譯成功之後我們可以看到生成的 16 進位的位元組碼文件 HelloWorld.class。
cafe babe 0000 0036 001b 0a00 0500 0e09 000f 0010 0a00 1100 1207 0013 0700 1401 0006 3c69 6e69 743e 0100 0328 2956 0100 0443 6f64 6501 000f 4c69 6e65 4e75 6d62 6572 5461 626c 6501 0004 6d61 696e 0100 1628 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 5601 000a 536f 7572 6365 4669 6c65 0100 0f48 656c 6c6f 576f 726c 642e 6a61 7661 0c00 0600 0707 0015 0c00 1600 1707 0018 0c00 1900 1a01 001d 636f 6d2f 736f 7574 6877 696e 642f 7465 7374 2f48 656c 6c6f 576f 726c 6401 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 0100 106a 6176 612f 6c61 6e67 2f53 7973 7465 6d01 0003 6f75 7401 0015 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b01 0013 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d01 0007 7072 696e 746c 6e01 0004 2849 2956 0021 0004 0005 0000 0000 0002 0001 0006 0007 0001 0008 0000 001d 0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0900 0000 0600 0100 0000 0300 0900 0a00 0b00 0100 0800 0000 3a00 0200 0400 0000 1210 0a3c 1014 3d1b 1c60 3eb2 0002 1db6 0003 b100 0000 0100 0900 0000 1600 0500 0000 0600 0300 0700 0600 0800 0a00 0900 1100 0a00 0100 0c00 0000 0200 0d
4、16 進位的文件我們根本看不出來任何的邏輯結構,所以此時需要對位元組碼文件進行反彙編,將 16 進位的內容反編譯成我們能看懂的 JVM 指令,這裡我們使用 javap -c 命令完成。
javap -c HelloWorld
5、反編譯之後的 JVM 指令如下所示。
Compiled from "HelloWorld.java" public class com.southwind.test.HelloWorld { public com.southwind.test.HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: istore_1 3: bipush 20 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 13: iload_3 14: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 17: return }
具體解釋一下上述的 JVM 指令。
第 1 行表示當前的位元組碼文件編譯自 HelloWorld.java。
第 3 行表示調用 HelloWorld 的無參構造函數來實例化當前對象。
第 4 行到第 7 行表示無參構造函數的執行流程。
第 5 行表示把 this 壓入操作數棧中。
第 6 行表示調用 HelloWorld 父類 Object 的無參構造,我們知道每個對象在實例化的時候都會默認先實例化其父類對象,並且默認調用父類的無參構造。
第 7 行 return 表示構造方法執行完畢。
第 10 行到第 22 行表示 main 方法的執行流程。
第 11 行表示將常量 10 壓入操作數棧。
第 12 行表示取出操作數棧棧頂元素,即 10,然後保存到局部變數表第 1 個位置,即變數 i。
第 13 行表示將常量 20 壓入操作數棧。
第 14 行表示取出操作數棧棧頂元素,即 20,然後保存到局部變數表第 2 個位置,即變數 j。
第 15 行表示將局部變數表第 1 個變數(i)壓入操作數棧。
第 16 行表示將局部變數表第 2 個變數(j)壓入操作數棧。
第 17 行表示取出操作數棧中的前兩個值相加,並將結果壓入操作數棧頂。
第 18 行表示取出操作數棧棧頂元素,保存到局部變數表第 3 個位置,即變數 k。
第 19 行表示讀取靜態實例 PrintStream。
第 20 行表示將局部變數表第 3 個變數(k)壓入操作數棧。
第 21 行表示調用 PrintStream 的 println 方法,將操作數棧頂元素(變數 k)輸出。
第 22 行 return 表示 main 方法執行完畢。