天天看點

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

Java虛拟機和真實的計算機一樣,運作的都是二進制的機器碼;而我們将.java 源代碼編譯成.class 檔案,class檔案便是Java虛拟機能夠認識的二進制機器碼,Java能夠識别class檔案中的資訊和機器指令,進而執行這些機器指令。那麼,Java虛拟機是如何運作這些二進制的機器碼的呢? 本文将通過一個非常簡單的例子,帶你感受一下Java虛拟機運作機器碼的過程和其工作的基本原理。

讀完本文,你将會了解到:

1、Java虛拟機對運作時虛拟機棧(JVM Stack) 的組織

2、方法調用過程是怎樣在JVM中表示的

3、JVM對一個方法執行的基本政策

4. JVM機器指令的格式

5. 機器指令的執行模式---基于操作數棧的模式

1. Java虛拟機對運作時虛拟機棧(JVM Stack)的組織

Java虛拟機在運作時會為每一個線程在記憶體中配置設定了一個虛拟機棧,來表示線程的運作狀态和資訊,虛拟機棧中的元素稱之為棧幀(JVM stack frame),每一個棧幀表示這對一個方法的調用資訊。如下所示:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

上述的描述可能會有點抽象,為了給讀者一個直覺的感受,我們定義一個簡單的Java類,然後執行這個運作這個類,逐漸分析整個Java虛拟機的運作時資訊的組織的。

2.  方法調用過程在JVM中是如何表示的

我們将定義如下帶有main方法的簡單類org.louis.jvm.codeset.Bootstrap.java ,逐漸分析該類在JVM中是如何表示的,方法是如何一步步運作的:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

當我們将Bootstrap.java 編譯成Bootstrap.class 并運作這段程式的時候,在JVM複雜的運作邏輯中,會有以下幾步:

1. 首先JVM會先将這個Bootstrap.class 資訊加載到 記憶體中的方法區(Method Area)中。

Bootstrap.class 中包含了常量池資訊,方法的定義 以及編譯後的方法實作的二進制形式的機器指令,所有的線程共享一個方法區,從中讀取方法定義和方法的指令集。

2. 接着,JVM會在Heap堆上為Bootstrap.class 建立一個Class執行個體用來表示Bootstrap.class 的 類執行個體。

3. JVM開始執行main方法,這時會為main方法建立一個棧幀,以表示main方法的整個執行過程(我會在後面章節中詳細展開這個過程);

4. main方法在執行的過程之中,調用了greeting靜态方法,則JVM會為greeting方法建立一個棧幀,推到虛拟機棧頂(我會在後面章節中詳細展開這個過程)。

5.當greeting方法運作完成後,則greeting方法出棧,main方法繼續運作;

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

JVM方法調用的過程是通過棧幀來實作的,那麼,方法的指令是如何運作的呢?弄清楚這個之前,我們要先了解對于JVM而言,方法的結構是什麼樣的。

我們知道,class 檔案時 JVM能夠識别的二進制檔案,其中通過特定的結構描述了每個方法的定義。

JVM在編譯Bootstrap.java 的過程中,在将源代碼編譯成二進制機器碼的同時,會判斷其中的每一個方法的三個資訊:

1 ).  在運作時會使用到的局部變量的數量(作用是:當JVM為方法建立棧幀的時候,在棧幀中為該方法建立一個局部變量表,來存儲方法指令在運算時的局部變量值)

2 ).  其機器指令執行時所需要的最大的操作數棧的大小(當JVM為方法建立棧幀的時候,在棧幀中為方法建立一個操作數棧,保證方法内指令可以完成工作)

3 ).  方法的參數的數量

經過編譯之後,我們可以得到main方法和greeting方法的資訊如下:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

注: 上述編譯後的資訊全部都存儲在Bootstrap.class 檔案中,并按照這Class檔案格式的形式存儲,關于Class檔案格式的定義,我在前幾篇文章中已經做了非常詳盡的介紹,如果您全部閱讀了,那麼相信您已經可以“讀懂” class 檔案了。如何讀懂class二進制檔案中關于method及其相應機器碼的組織,請閱讀《Java虛拟機原理圖解》1.5、 class檔案中的方法表集合--method方法在class檔案中是怎樣組織的。

JVM運作main方法的過程:

1.為main方法建立棧幀:

JVM解析main方法,發現其 局部變量的數量為 2,操作數棧的數量為1, 則會為main方法建立一個棧幀(VM Stack),并将其加入虛拟機棧中:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

2. 完成棧幀初始化:

main棧幀建立完成後,會将棧幀push 到虛拟機棧中,現在有兩步重要的事情要做:

a). 計算PC值。PC 是指令計數器,其内部的值決定了JVM虛拟機下一步應該執行哪一個機器指令,而機器指令存放在方法區,我們需要讓PC的值指向方法區的main方法上;

初始化 PC = main方法在方法區指令的位址+0;

b). 局部變量的初始化。main方法有個入參(String[] args) ,JVM已經在main所在的棧幀的局部變量表中為其空出來了一個slot ,我們需要将 args 的引用值初始化到局部點亮表中;

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

接着JVM開始讀取PC指向的機器指令。如上圖所示,main方法的指令序列:12 10 4c 2b b8 20 12 b1 ,通過JVM虛拟機指令集規範,可以将這個指令序列解析成以下Java彙編語言:

機器指令

彙編語言

解釋

對棧幀的影響

0x12 0x10

ldc #16

将常量池中第16個常量池項引用推到操作數棧棧頂。常量池第16項是CONSTANT_UTF-8_INFO項,表示”Louis”字元串

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

0x4c

astore_1

操作數棧的棧頂元素出棧,将棧頂元素的值賦給index=1 的局部變量表元素上。

這裡等價于:name = “Louis”.

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

0x2b

aload_1

将局部變量表中index=1的元素的值推到操作數棧棧頂

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

0xb8 0x20 0x12

invokestatic #18

0xb8表示機器指令invokestatic,操作數是0x20 << 8| 0x12 = 18,操作數18表示指向常量池第18項,該項是main方法的符号引用:

org/louis/jvm/codeset/Bootstrap.greeting:(Ljava/lang/String;)V

當JVM執行這條語句的時候,會做以下幾件事:

a).方法符号引用校驗。會校驗這個方法的符号引用,按照這個符号規則 在常量池中查找是否有這個方法的定義,如果找到了此方法的定義,則表示解析成功。如果是方法greeting:(Ljava/lang/String;)V沒有找到,JVM會抛出錯誤NoSuchMethodError

b).為新的方法調用建立新的棧幀。然後JVM會為此方法greeting建立一個新的棧幀(VM stack),并根據greeting中操作數棧的大小和局部變量的數量分别建立相應大小的操作數棧;然後将此棧幀推到虛拟機棧的棧頂。

c).更新PC指令計數器的值。将目前PC程式計數器的值記錄到greeting棧幀中,當greeting執行完成後,以便恢複PC值。更新PC的值,使下一條執行的指令位址指向greeting方法的指令開始部分。

這條語句會使目前的main方法執行暫停,使JVM進入對greeting方法的執行當中當greeting方法執行完成後,才會恢複PC程式計數器的值指向目前下一條指令。

0xb1

return

傳回

當main方法調用greeting()時, JVM會為greeting方法建立一個棧幀,用以表示對greeting方法的調用,具體棧幀資訊如下:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

具體的greeting方法的機器碼表示的含義如下圖所示:

機器指令

彙編語言

解釋

常量池引用

b2 20 1a

getstatic     #26

擷取指定類的靜态域,并将其值壓入棧頂.将常量池中的第26個符号引用推到操作數棧中:

#26:// Field java/lang/System.out:Ljava/io/PrintStream;

bb 20 20

new           #32

建立一個對象,并将其引用值壓入棧頂。建立一個java/lang/StringBuider執行個體,将其壓入棧頂。

#32:// class java/lang/StringBuilder

59

dup

複制操作數棧棧頂的值,并插入到棧頂

12 22

ldc           #34

從運作時常量池中提取資料推入操作數棧将“Hello” String引用複制到 操作數棧中

#34:// String Hello,

b7 20 24

invokespecial #36

調用超類構造方法,執行個體初始化方法,私有方法。此處調用StringBuilder(String)構造方法,并将結果推到棧頂

#36:// Method java/lang/StringBuilder."":(Ljava/lang/String;)V

2a

invokevirtual #38

調用超類構造方法,執行個體初始化方法,私有方法。StringBuilder執行個體的 append(String ) 方法,表示:"Hello,"+"Louis".

// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

b6 20 2a

invokevirtual #42

調用超類構造方法,執行個體初始化方法,私有方法。調用StringBuilder執行個體的toString()方法,結果保留在棧頂。

// Method java/lang/StringBuilder.toString:()Ljava/lang/String;

b6 20 2e

invokevirtual #46

調用超類構造方法,執行個體初始化方法,私有方法。調用System.out.println(String)方法

// Method java/io/PrintStream.println:(Ljava/lang/String;)V

b1

return

結束傳回

3.  JVM對一個方法執行的基本政策

一般地,對于java方法的執行,在JVM在其某一特定線程的虛拟機棧(JVM Stack) 中會為方法配置設定一個 局部變量表,一個操作數棧,用以存儲方法的運作過程中的中間值存儲。

由于JVM的指令是基于棧的,即大部分的指令的執行,都伴随着操作數的出棧和入棧。是以在學習JVM的機器指令的時候,一定要銘記一點:

每個機器指令的執行,對操作數棧和局部變量的影響,充分地了解了這個機制,你就可以非常順暢地讀懂class檔案中的二進制機器指令了。

如下是棧幀資訊的簡化圖,在分析JVM指令時,腦海中對棧幀有個清晰的認識:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

4.  機器指令的格式

所謂的機器指令,就是隻有機器才能夠認識的二進制代碼。一個機器指令分為兩部分組成:

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

注:

a).  如上圖所示JVM虛拟機的操作碼是由一個位元組組成的,也就是說對于JVM虛拟機而言,其指令的數量最多為 2^8,即 256個;

b). 上圖中的操作碼如:b2,bb,59....等等都是表示某一特定的機器指令,為了友善我們識别,其分别有相應的助記符:getstatic,new,dup.... 這樣友善我們了解。

5.  機器指令的執行模式---基于操作數棧的模式

對于傳統的實體機而言,大部分的機器指令的設計都是寄存器的,實體機内設定若幹個寄存器,用以存儲機器指令運作過程中的值,寄存器的數量和支援的指令的個數決定了這個機器的處理能力。

但是Java虛拟機的設計的機制并不是這樣的,Java虛拟機使用操作數棧 來存儲機器指令的運算過程中的值。所有的操作數的操作,都要遵循出棧和入棧的規則,是以在《Java虛拟機規範》中,你會發現有很多機器指令都是關于出棧入棧的操作。

java 虛拟機 指令_java虛拟機詳細圖解9--JVM機器指令集

本文旨在介紹JVM虛拟機指令的運作原理,如果你想更深入地了解指令集的資訊以及使用注意事項,請您閱讀《Java虛拟機規範(Java Virtual Machine Specification)》 關于機器指令集的詳細定義。

機器指令彙編語言解釋對棧幀的影響0x12 0x10ldc #16将常量池中第16個常量池項引用推到操作數棧棧頂。常量池第16項是CONSTANT_UTF-8_INFO項,表示”Louis”字元串0x4castore_1操作數棧的棧頂元素出棧,将棧頂元素的值賦給index=1 的局部變量表元素上。

這裡等價于:name = “Louis”.0x2baload_1将局部變量表中index=1的元素的值推到操作數棧棧頂0xb8 0x20 0x12invokestatic #180xb8表示機器指令invokestatic,操作數是0x20 << 8| 0x12 = 18,操作數18表示指向常量池第18項,該項是main方法的符号引用:org/louis/jvm/codeset/Bootstrap.greeting:(Ljava/lang/String;)V 當JVM執行這條語句的時候,會做以下幾件事:a).方法符号引用校驗。會校驗這個方法的符号引用,按照這個符号規則 在常量池中查找是否有這個方法的定義,如果找到了此方法的定義,則表示解析成功。如果是方法greeting:(Ljava/lang/String;)V沒有找到,JVM會抛出錯誤NoSuchMethodErrorb).為新的方法調用建立新的棧幀。然後JVM會為此方法greeting建立一個新的棧幀(VM stack),并根據greeting中操作數棧的大小和局部變量的數量分别建立相應大小的操作數棧;然後将此棧幀推到虛拟機棧的棧頂。c).更新PC指令計數器的值。将目前PC程式計數器的值記錄到greeting棧幀中,當greeting執行完成後,以便恢複PC值。更新PC的值,使下一條執行的指令位址指向greeting方法的指令開始部分。這條語句會使目前的main方法執行暫停,使JVM進入對greeting方法的執行當中當greeting方法執行完成後,才會恢複PC程式計數器的值指向目前下一條指令。 0xb1return傳回    ————————————————版權聲明:本文為CSDN部落客「亦山」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。原文連結:https://blog.csdn.net/u010349169/java/article/details/50412126