騰訊三面:什么是 JVM 字節(jié)碼?它是如何工作的?
作為 Java程序員都知道 Java是跨平臺(tái)的語(yǔ)言,編譯一次到處運(yùn)行,這得益于 JVM字節(jié)碼,這篇文章,我們將一起分析什么是JVM字節(jié)碼?如何查看 JVM字節(jié)碼?JVM字節(jié)碼是如何工作的?
什么 JVM 字節(jié)碼?
Java 源代碼經(jīng)過(guò)編譯器編譯后,就會(huì)生成 JVM 字節(jié)碼,它是一種基于棧的低級(jí)、中立于平臺(tái)的指令架構(gòu),每個(gè)字節(jié)碼指令都會(huì)在 JVM 上執(zhí)行一系列的操作,如加載、存儲(chǔ)、運(yùn)算、跳轉(zhuǎn)等。它使用基于操作數(shù)棧和局部變量表的執(zhí)行模型。
JVM 字節(jié)碼具有以下特點(diǎn):
- 獨(dú)立于具體的硬件和操作系統(tǒng),不同平臺(tái)上的 JVM 可以解釋和執(zhí)行相同的字節(jié)碼文件。
- 相對(duì)于機(jī)器碼和源代碼,JVM 字節(jié)碼是一種更高級(jí)別的抽象,并且比機(jī)器碼更容易閱讀和編寫。
- JVM 字節(jié)碼通過(guò)運(yùn)行時(shí)的即時(shí)編譯器或解釋器執(zhí)行。
因此,只要在不同平臺(tái)上安裝相應(yīng)的 JVM,就能在這些平臺(tái)上運(yùn)行相同的字節(jié)碼,這種特性為 Java 程序提供了很高的可移植性和兼容性。值得注意的是,其他編程語(yǔ)言也可以編譯成 JVM 字節(jié)碼,利用 JVM 的優(yōu)勢(shì)。這些編程語(yǔ)言叫做基于 JVM 的語(yǔ)言,例如 Kotlin、Groovy 等。
如何查看 JVM 字節(jié)碼?
通過(guò) javap -c ClassName指令就可以查看 JVM字節(jié)碼,為了更好的說(shuō)明,下面通過(guò)一個(gè)簡(jiǎn)單的 Java程序和對(duì)應(yīng)的 JVM字節(jié)碼示例來(lái)進(jìn)行演示:
1.示例代碼
如下代碼,在控制臺(tái)輸出“Hello, World”:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
使用 javac 命令編譯上述 Java 源代碼后會(huì)生成一個(gè) HelloWorld.class 文件,然后使用javap -c HelloWorld命令查看字節(jié)碼,內(nèi)容如下:
Compiled from "HelloWorld.java"
public class HelloWorld {
public 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: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
2.字節(jié)碼解釋
(1) 構(gòu)造方法 HelloWorld()
- aload_0: 加載局部變量表中第一個(gè)變量(即this引用)。
- invokespecial #1: 調(diào)用父類(java/lang/Object)的構(gòu)造方法。
- return: 從構(gòu)造方法返回。
(2) main方法
- getstatic #2: 獲取靜態(tài)字段java/lang/System.out,它是一個(gè) PrintStream 對(duì)象。
- ldc #3: 將常量池中索引為3的項(xiàng)(即字符串"Hello, World!")加載到操作數(shù)棧。
- invokevirtual #4: 調(diào)用 PrintStream 的 println 方法,參數(shù)是棧頂?shù)淖址?/li>
- return: 從main方法返回。
(3) 關(guān)鍵字節(jié)碼指令解析
- aload_0: 加載局部變量表中索引為 0的引用類型變量到操作數(shù)棧。
- invokespecial: 調(diào)用實(shí)例初始化方法和私有方法。
- getstatic: 獲取靜態(tài)字段的值并將其壓入操作數(shù)棧。
- ldc: 將常量池中的常量加載到操作數(shù)棧。
- invokevirtual: 調(diào)用對(duì)象的實(shí)例方法,方法的選擇是基于對(duì)象的運(yùn)行時(shí)類型。
通過(guò)這個(gè)示例,我們可以看到 Java源代碼被編譯成 JVM 字節(jié)碼后是什么樣子。
JVM字節(jié)碼指令集
通過(guò)上述查看 JVM字節(jié)碼的示例,我們可以看到很多 JVM內(nèi)部的指令,比如加載、存儲(chǔ)、運(yùn)算、跳轉(zhuǎn)等。JVM字節(jié)碼指令集(Bytecode Instruction Set)是 JVM用來(lái)執(zhí)行 Java 程序的指令集合,每條字節(jié)碼指令由一個(gè)字節(jié)的操作碼(opcode)和可選的操作數(shù)組成。
以下是 JVM 字節(jié)碼指令集的一些主要類別和具體指令:
1.加載和存儲(chǔ)指令
加載和存儲(chǔ)指令,全稱 Load and Store Instructions,包含以下幾個(gè)指令:
- aload: 從局部變量表加載引用類型變量到操作數(shù)棧。
- astore: 將操作數(shù)棧頂?shù)囊妙愋妥兞看鎯?chǔ)到局部變量表。
- iload: 從局部變量表加載整數(shù)類型變量到操作數(shù)棧。
- istore: 將操作數(shù)棧頂?shù)恼麛?shù)類型變量存儲(chǔ)到局部變量表。
- dload, fload, lload: 加載雙精度浮點(diǎn)數(shù)、單精度浮點(diǎn)數(shù)和長(zhǎng)整數(shù)類型變量。
- dstore, fstore, lstore: 存儲(chǔ)雙精度浮點(diǎn)數(shù)、單精度浮點(diǎn)數(shù)和長(zhǎng)整數(shù)類型變量。
2.算術(shù)運(yùn)算指令
算術(shù)運(yùn)算指令,全稱 Arithmetic Instructions,包含以下幾個(gè)指令:
- iadd: 對(duì)棧頂?shù)膬蓚€(gè)整數(shù)進(jìn)行加法運(yùn)算。
- isub: 對(duì)棧頂?shù)膬蓚€(gè)整數(shù)進(jìn)行減法運(yùn)算。
- imul: 對(duì)棧頂?shù)膬蓚€(gè)整數(shù)進(jìn)行乘法運(yùn)算。
- idiv: 對(duì)棧頂?shù)膬蓚€(gè)整數(shù)進(jìn)行除法運(yùn)算。
- iinc: 對(duì)局部變量表中的整數(shù)變量進(jìn)行自增。
- dadd, fadd, ladd: 加法運(yùn)算(雙精度浮點(diǎn)數(shù)、單精度浮點(diǎn)數(shù)、長(zhǎng)整數(shù))。
- dsub, fsub, lsub: 減法運(yùn)算(雙精度浮點(diǎn)數(shù)、單精度浮點(diǎn)數(shù)、長(zhǎng)整數(shù))。
3.類型轉(zhuǎn)換指令
類型轉(zhuǎn)換指令,全稱 Type Conversion Instructions,包含以下幾個(gè)指令:
- i2d: 整數(shù)轉(zhuǎn)雙精度浮點(diǎn)數(shù)。
- i2f: 整數(shù)轉(zhuǎn)單精度浮點(diǎn)數(shù)。
- i2l: 整數(shù)轉(zhuǎn)長(zhǎng)整數(shù)。
- d2i, f2i, l2i: 轉(zhuǎn)換為整數(shù)。
4.對(duì)象操作指令
對(duì)象操作指令,全稱 Object Manipulation Instructions,包含以下幾個(gè)指令:
- new: 創(chuàng)建一個(gè)新的對(duì)象實(shí)例。
- newarray: 創(chuàng)建一個(gè)新的數(shù)組。
- anewarray: 創(chuàng)建一個(gè)新的引用類型數(shù)組。
- checkcast: 檢查對(duì)象是否為某一類型的實(shí)例。
- instanceof: 判斷對(duì)象是否是某一類型的實(shí)例。
5.方法調(diào)用和返回指令
方法調(diào)用和返回指令,全稱 Method Invocation and Return Instructions,包含以下幾個(gè)指令:
- invokestatic: 調(diào)用靜態(tài)方法。
- invokevirtual: 調(diào)用實(shí)例方法,根據(jù)對(duì)象的實(shí)際類型進(jìn)行分派。
- invokespecial: 調(diào)用實(shí)例初始化方法、私有方法和父類方法。
- invokeinterface: 調(diào)用接口方法。
- return: 從方法返回(無(wú)返回值)。
- ireturn, dreturn, freturn, lreturn, areturn: 從方法返回(返回值為整數(shù)、雙精度浮點(diǎn)數(shù)、單精度浮點(diǎn)數(shù)、長(zhǎng)整數(shù)、引用類型)。
6.控制流指令
控制流指令,全稱 Control Flow Instructions,包含以下幾個(gè)指令:
- goto: 無(wú)條件跳轉(zhuǎn)。
- ifeq: 如果棧頂整數(shù)為0,則跳轉(zhuǎn)。
- ifne: 如果棧頂整數(shù)不為0,則跳轉(zhuǎn)。
- iflt, ifge, ifgt, ifle: 比較棧頂整數(shù),并根據(jù)結(jié)果跳轉(zhuǎn)。
- tableswitch: 用于switch語(yǔ)句的多路分支跳轉(zhuǎn)。
- lookupswitch: 用于switch語(yǔ)句的查找表跳轉(zhuǎn)。
7.異常處理指令
異常處理指令,全稱 Exception Handling Instructions,包含以下幾個(gè)指令:
- athrow: 拋出異?;蝈e(cuò)誤。
- try-catch塊:通過(guò)異常表實(shí)現(xiàn),不是具體的字節(jié)碼指令。
8.同步指令
同步指令,全稱 Synchronization Instructions,包含以下幾個(gè)指令:
- monitorenter: 獲取對(duì)象的監(jiān)視器鎖。
- monitorexit: 釋放對(duì)象的監(jiān)視器鎖。
9.棧操作指令
棧操作指令,全稱 Stack Operations Instructions,包含以下幾個(gè)指令:
- pop: 彈出棧頂?shù)囊粋€(gè)元素。
- dup: 復(fù)制棧頂?shù)囊粋€(gè)元素。
- swap: 交換棧頂?shù)膬蓚€(gè)元素。
JVM 如何執(zhí)行字節(jié)碼?
JVM 字節(jié)碼的執(zhí)行過(guò)程主要依賴于 Java 虛擬機(jī)的解釋器和即時(shí)編譯器(Just-In-Time Compiler,簡(jiǎn)稱JIT)。JVM會(huì)將字節(jié)碼讀取到內(nèi)存中,并逐條解釋執(zhí)行,或者將熱點(diǎn)代碼編譯為機(jī)器碼來(lái)提高執(zhí)行效率。
為了更好地說(shuō)明 JVM 字節(jié)碼的執(zhí)行過(guò)程,我們還是通過(guò)一個(gè)具體的示例來(lái)進(jìn)行說(shuō)明。
1.示例代碼
這里以 a + b 求和為例,代碼如下:
public class Sum {
public static int add(int a, int b) {
return a + b;
}
}
使用 javap -c Sum 命令獲取字節(jié)碼,具體信息如下:
Compiled from "Sum.java"
public class Sum {
public Sum();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}
2.字節(jié)碼解釋
(1) 構(gòu)造方法 Sum()
- aload_0: 加載局部變量表中第一個(gè)變量(即this引用)。
- invokespecial #1: 調(diào)用父類(java/lang/Object)的構(gòu)造方法。
- return: 從構(gòu)造方法返回。
(2) add()方法
- iload_0: 加載局部變量表中索引為0的整數(shù)(即參數(shù)a)到操作數(shù)棧。
- iload_1: 加載局部變量表中索引為1的整數(shù)(即參數(shù)b)到操作數(shù)棧。
- iadd: 彈出操作數(shù)棧頂?shù)膬蓚€(gè)整數(shù),進(jìn)行加法運(yùn)算,并將結(jié)果壓入操作數(shù)棧。
- ireturn: 從方法返回,并將操作數(shù)棧頂?shù)恼麛?shù)作為返回值。
3.執(zhí)行過(guò)程
假設(shè)我們?cè)诹硪粋€(gè)類中調(diào)用Sum.add(2, 3),執(zhí)行過(guò)程如下:
- JVM將參數(shù) 2和 3壓入局部變量表,iload_0指令將參數(shù) 2加載到操作數(shù)棧。
- iload_1指令將參數(shù) 3加載到操作數(shù)棧。
- iadd指令彈出操作數(shù)棧頂?shù)膬蓚€(gè)值(2和3),進(jìn)行加法運(yùn)算,將結(jié)果5壓入操作數(shù)棧。
- ireturn指令將操作數(shù)棧頂?shù)闹担?)作為返回值返回給調(diào)用者。
總結(jié)
本文,我們分析了什么是JVM字節(jié)碼,如何查看JVM字節(jié)碼以及JVM如何執(zhí)行字節(jié)碼,掌握這些底層不但可以幫助我們更好的理解,為什么 Java可以編譯一次,到處運(yùn)行,還可以幫助我們更好的了解 Java的運(yùn)行機(jī)制以及理解 Java的編程精髓。