java虚拟_Java虚拟机(JVM)工作原理
雖然本教程的內(nèi)容為 x86 處理器的原生匯編語(yǔ)言,但是了解其他機(jī)器架構(gòu)如何工作也是有益的。JVM 是基于堆棧機(jī)器的首選示例。JVM 用堆棧實(shí)現(xiàn)數(shù)據(jù)傳送、算術(shù)運(yùn)算、比較和分支操作,而不是用寄存器來(lái)保存操作數(shù)(如同 x86 一樣)。
數(shù)據(jù)結(jié)構(gòu),讓它們協(xié)同工作。Java 字節(jié)碼是指編譯好的 Java 程序中使用的機(jī)器語(yǔ)言的名字。
JVM 執(zhí)行的編譯程序包含了 Java 字節(jié)碼。每個(gè) Java 源程序都必須編譯為 Java 字節(jié)碼(形式為 .class 文件)后才能執(zhí)行。包含 Java 字節(jié)碼的程序可以在任何安裝了 Java 運(yùn)行時(shí)軟件的計(jì)算機(jī)系統(tǒng)上執(zhí)行。
例如,一個(gè) Java 源文件名為 Account.java,編譯為文件 Account.class。這個(gè)類文件是該類中每個(gè)方法的字節(jié)碼流。JVM 可能選擇實(shí)時(shí)編譯(just-in-time compilation)技術(shù)把類字節(jié)碼編譯為計(jì)算機(jī)的本機(jī)機(jī)器語(yǔ)言。
正在執(zhí)行的 Java 方法有自己的堆棧幀存放局部變量、操作數(shù)棧、輸入?yún)?shù)、返回地址和返回值。操作數(shù)區(qū)實(shí)際位于堆棧頂端,因此,壓入這個(gè)區(qū)域的數(shù)值可以作為算術(shù)和邏輯運(yùn)算的操作數(shù),以及傳遞給類方法的參數(shù)。
在局部變量被算術(shù)運(yùn)算指令或比較指令使用之前,它們必須被壓入堆棧幀的操作數(shù)區(qū)域。通常把這個(gè)區(qū)域稱為操作數(shù)棧(operand stack)。
Java 字節(jié)碼中,每條指令包含 1 字節(jié)的操作碼、零個(gè)或多個(gè)操作數(shù)。操作碼可以用 Java 反匯編工具顯示名字,如 iload、istore、imul 和 goto。每個(gè)堆棧項(xiàng)為 4 字節(jié)(32 位)。
查看反匯編字節(jié)碼
Java 開(kāi)發(fā)工具包(JDK)中的工具 javap.exe 可以顯示 java.class 文件的字節(jié)碼,這個(gè)操作被稱為文件的反匯編。命令行語(yǔ)法如下所示:
javap -c classname
比如,若類文件名為 Account.class,則相應(yīng)的 javap 命令行為:
javap -c Account
安裝 Java 開(kāi)發(fā)工具包后,可以在 \bin 文件夾下找到 javap.exe 工具。
指令集
1) 基本數(shù)據(jù)類型
JVM 可以識(shí)別 7 種基本數(shù)據(jù)類型,如下表所示。和 x86 整數(shù)一樣,所有有符號(hào)整數(shù)都是二進(jìn)制補(bǔ)碼形式。但它們是按照大端順序存放的,即高位字節(jié)位于每個(gè)整數(shù)的起始地址(x86 的整數(shù)按小端順序存放)。
數(shù)據(jù)類型所占字節(jié)格式數(shù)據(jù)類型所占字節(jié)格式
char
2
Unicode 字符
long
8
有符號(hào)整數(shù)
byte
1
有符號(hào)整數(shù)
float
4
IEEE 單精度實(shí)數(shù)
short
2
有符號(hào)整數(shù)
double
8
IEEE 雙精度實(shí)數(shù)
int
4
有符號(hào)整數(shù)
2) 比較指令
比較指令從操作數(shù)棧的頂端彈出兩個(gè)操作數(shù),對(duì)它們進(jìn)行比較,再把比較結(jié)果壓入堆棧。現(xiàn)在假設(shè)操作數(shù)入棧順序如下所示:
下表給出了比較 op1 和 op2 之后壓入堆棧的數(shù)值:
op1 和 op2 比較的結(jié)果壓入操作數(shù)棧的數(shù)值
op1 > op2
1
op1 = op2
0
op1 < op2
-1
dcmp 指令比較雙字,fcmp 指令比較浮點(diǎn)數(shù)。
3) 分支指令
分支指令可以分為有條件分支和無(wú)條件分支。Java 字節(jié)碼中無(wú)條件分支的例子是 goto 和 jsr。
goto 指令無(wú)條件分支到一個(gè)標(biāo)號(hào):
goto label
jsr 指令調(diào)用用標(biāo)號(hào)定義的子程序。其語(yǔ)法如下:
jsr label
條件分支指令通常檢測(cè)從操作數(shù)棧頂彈出的數(shù)值。根據(jù)該值,指令決定是否分支到給定標(biāo)號(hào)。比如,ifle 指令就是當(dāng)彈出數(shù)值小于等于 0 時(shí)跳轉(zhuǎn)到標(biāo)號(hào)。其語(yǔ)法如下:
ifle label
同樣,ifgt 指令就是當(dāng)彈出數(shù)值大于等于 0 時(shí)跳轉(zhuǎn)到標(biāo)號(hào)。其語(yǔ)法如下:
ifgt label
Java 反匯編示例
為了幫助理解 Java 字節(jié)碼是如何工作的,本節(jié)將給出用 Java 編寫(xiě)的一些短代碼例子。在這些例子中,請(qǐng)注意不同版本 Java 的字節(jié)碼清單細(xì)節(jié)會(huì)存在些許差異。
【示例 1】?jī)蓚€(gè)整數(shù)相加
下面的 Java 源代碼行實(shí)現(xiàn)兩個(gè)整數(shù)相加,并將和數(shù)保存在第三個(gè)變量中:
int A = 3;
int B = 2;
int sum = 0;
sum = A + B;
該 Java 代碼的反匯編如下:
iconst_3
istore_0
iconst_2
istore_l
iconst_0
istore_2
iload_0
iload_l
iadd
istore_2
每個(gè)編號(hào)行表示一條 Java 字節(jié)碼指令的字節(jié)偏移量。本例中,可以發(fā)現(xiàn)每條指令都只占一個(gè)字節(jié),因?yàn)橹噶钇屏康木幪?hào)是連續(xù)的。
盡管字節(jié)碼反匯編一般不包括注釋,這里還是會(huì)將注釋添加上去。雖然局部變量在運(yùn)行時(shí)堆棧中有專門(mén)的保留區(qū)域,但是指令在執(zhí)行算術(shù)運(yùn)算和數(shù)據(jù)傳送時(shí)還會(huì)使用另一個(gè)堆棧,即操作數(shù)棧。為了避免在這兩個(gè)堆棧間產(chǎn)生混淆,將用索引值來(lái)指代變量位置,如 0、1、2 等。
現(xiàn)在來(lái)仔細(xì)分析剛才的字節(jié)碼。開(kāi)始的兩條指令將一個(gè)常數(shù)值壓入操作數(shù)棧,并把同一個(gè)值彈出到位置為 0 的局部變量:
iconst_3 //常數(shù)(3)壓入操作數(shù)棧
istore_0 //彈出到局部變量0
接下來(lái)的四行將其他兩個(gè)常數(shù)壓入操作數(shù)棧,并把它們彈岀到位置分別為 1 和 2 的局部變量:
iconst_2 //常數(shù)(2)壓入操作數(shù)棧
istore_1 //彈出到局部變量1
iconst_0 //常數(shù)(0)壓入操作數(shù)棧
istore_2 //彈出到局部變量2
由于已經(jīng)知道了該生成字節(jié)碼的 Java 源代碼,因此,很明顯下表列出的是三個(gè)變量的位置索引:
位置索引變量名
0
A
1
B
2
sum
接下來(lái),為了實(shí)現(xiàn)加法,必須將兩個(gè)操作數(shù)壓入操作數(shù)棧。指令 iload_0 將變量 A 入棧,指令 iload_1 對(duì)變量 B 進(jìn)行相同的操作:
iload_0 // (A 入棧)
iload_1 // (B 入棧)
現(xiàn)在操作數(shù)棧包含兩個(gè)數(shù):
這里并不關(guān)心這些例子的實(shí)際機(jī)器表示,因此上圖中的運(yùn)行時(shí)堆棧是向上生長(zhǎng)的。每個(gè)堆棧示意圖中的最大值即為棧頂。
指令 iadd 將棧頂?shù)膬蓚€(gè)數(shù)相加,并把和數(shù)壓入堆棧:
iadd
操作數(shù)棧現(xiàn)在包含的是 A、B 的和數(shù):
指令 istore_2 將棧頂內(nèi)容彈出到位置為 2 的變量,其變量名為 sum:
istore_2
操作數(shù)棧現(xiàn)在為空。
【示例 2】?jī)蓚€(gè) Double 類型數(shù)據(jù)相加
下面的 Java 代碼片段實(shí)現(xiàn)兩個(gè) double 類型的變量相加,并將和數(shù)保存到 sum。它執(zhí)行的操作與兩個(gè)整數(shù)相加示例相同,因此這里主要關(guān)注的是整數(shù)處理與 double 處理的差異:
double A = 3.1;
double B = 2;
double sum = A + B;
本例的反匯編字節(jié)碼如下所示,用 javap 實(shí)用程序可以在右邊插入注釋:
ldc2_w #20; // double 3.Id
dstore_0
ldc2_w #22; // double 2.Od
dstore_2
dload_0
dload_2
dadd
dstore_4
下面對(duì)這個(gè)代碼進(jìn)行分步討論。偏移量為 0 的指令 ldc2_w 把一個(gè)浮點(diǎn)常數(shù)(3.1)從常數(shù)池壓入操作數(shù)棧。ldc2 指令總是用兩個(gè)字節(jié)作為常數(shù)池區(qū)域的索引:
ldc2_w #20; // double 3.ld
偏移量為 3 的 dstore 指令從堆棧彈出一個(gè) double 數(shù),送入位置為 0 的局部變量。該指令起始偏移量(3)反映出第一條指令占用的字節(jié)數(shù)(操作碼加上兩字節(jié)索引):
dstore_0 //保存到 A
同樣,接下來(lái)偏移量為 4 和 7 的兩條指令對(duì)變量 B 進(jìn)行初始化:
ldc2_w #22; // double 2.Od
dstore_2 // 保存到 B
指令 dload_0 和 dload_2 把局部變量入棧,其索引指的是 64 位位置(兩個(gè)變量棧項(xiàng)),因?yàn)殡p字?jǐn)?shù)值要占用 8 個(gè)字節(jié):
dload_0
dload_2
接下來(lái)的指令(dadd)將棧頂?shù)膬蓚€(gè) double 值相加,并把和數(shù)入棧:
dadd
最后,指令 dstore_4 把棧頂內(nèi)容彈出到位置為 4 的局部變量:
dstore_4
JVM 條件分支
了解 JVM 怎樣處理?xiàng)l件分支是理解 Java 字節(jié)碼的重要一環(huán)。比較操作總是從堆棧棧頂彈出兩個(gè)數(shù)據(jù),對(duì)它們進(jìn)行比較后,再把結(jié)果數(shù)值入棧。條件分支指令常常跟在比較操作的后面,利用棧頂數(shù)值決定是否分支到目標(biāo)標(biāo)號(hào)。比如,下面的 Java 代碼包含一個(gè)簡(jiǎn)單的 IF 語(yǔ)句,它將兩個(gè)數(shù)值中的一個(gè)分配給一個(gè)布爾變量:
double A = 3.0;
boolean result = false;
if( A > 2.0 )
result = false;
else
result = true;
該 Java 代碼對(duì)應(yīng)的反匯編如下所示:
ldc2_w #26; // double 3.Od
dstore_0 // 彈出到 A
iconst_0 // false = 0
istore_2 //保存到 result
dload_0
ldc2_w #22; // double 2.0d
dcmpl
ifle 19 //如果 A ≤ 2.0,轉(zhuǎn)到 19
iconst_0 // false
istore_2 // result = false
goto 21 //跳過(guò)后面兩條語(yǔ)句
iconst_l // true
istore_2 // result = true
開(kāi)始的兩條指令將 3.0 從常數(shù)池復(fù)制到運(yùn)行時(shí)堆棧,再把它從堆棧彈岀到變量 A:
ldc2_w #26; // double 3.0d
dstore_0 // 彈出至A
接下來(lái)的兩條指令將布爾值 false (等于 0)從常量區(qū)復(fù)制到堆棧,再把它彈出到變量 result:
iconst_0 // false = 0
istore_2 // 保存到 result
A 的值(位置 0)壓入操作數(shù)棧,數(shù)值 2.0 緊跟其后入棧:
dload_0???? //A 入棧
ldc2_w #22; // double 2.0d
操作數(shù)棧現(xiàn)在有兩個(gè)數(shù)值:
指令 dcmpl 將兩個(gè) double 數(shù)彈出堆棧進(jìn)行比較。由于棧頂?shù)臄?shù)值(2.0)小于它下面的數(shù)值(3.0),因此整數(shù) 1 被壓入堆棧。
dcmpl
如果從堆棧彈出的數(shù)值小于等于 0,則指令 ifle 就分支到給定的偏移量:
ifle 19?? //如果 stack.pop() <= 0,轉(zhuǎn)到 19
這里要回顧一下之前給出的 Java 源代碼示例,若 A>2.0,其分配的值為 false:
if( A > 2.0 )
result = false;
else
result = true;
如果 A <= 2.0,Java 字節(jié)碼就把 IF 語(yǔ)句轉(zhuǎn)向偏移量為 19 的語(yǔ)句,為 result 分配數(shù)值 true。與此同時(shí),如果不發(fā)生到偏移量 19 的分支,則由下面幾條指令把 false 賦給 result:
iconst_0? ? ?// false
istore_2? ? ?// result = false
goto 21???? //跳過(guò)后面兩條指令
偏移量 16 的指令 goto 跳過(guò)后面兩行代碼,它們的作用是給 result 分配 true:
iconst_l // true
istore_2 // result = true
Java 虛擬機(jī)的指令集與 x86 處理器系列的指令集有很大的不同。它采用面向堆棧的方法實(shí)現(xiàn)計(jì)算、比較和分支,與 x86 指令經(jīng)常使用寄存器和內(nèi)存操作數(shù)形成了鮮明的對(duì)比。
雖然字節(jié)碼的符號(hào)反匯編不如 x86 匯編語(yǔ)言簡(jiǎn)單,但是,編譯器生成字節(jié)碼也是相當(dāng)容易的。每個(gè)操作都是原子的,這就意味著它只執(zhí)行一個(gè)操作。
若 JVM 使用的是實(shí)時(shí)編譯器,則 Java 字節(jié)碼只要在執(zhí)行前轉(zhuǎn)換為本地機(jī)器語(yǔ)言即可。就這方面來(lái)說(shuō),Java 字節(jié)碼與基于精簡(jiǎn)指令集(RISC)模型的機(jī)器語(yǔ)言有很多共同點(diǎn)。
總結(jié)
以上是生活随笔為你收集整理的java虚拟_Java虚拟机(JVM)工作原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Shell else if mysql_
- 下一篇: java for循环break_Java