1. 程式人生 > 實用技巧 >java虛擬機器詳細圖解9--JVM機器指令集

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

宣告:本文摘抄自:https://blog.csdn.net/u010349169/article/details/50412126

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虛擬機器的執行時資訊的組織的。

2. 方法呼叫過程在JVM中是如何表示的

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

  

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

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

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

  2. 接著,JVM會在Heap堆上為Bootstrap.class 建立一個Class<Bootstrap>例項用來表示Bootstrap.class 的 類例項。

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

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

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

  

  JVM方法呼叫的過程是通過棧幀來實現的,那麼,方法的指令是如何執行的呢?弄清楚這個之前,我們要先了解對於JVM而言,方法的結構是什麼樣的。

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

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

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

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

  3 ). 方法的引數的數量

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

  

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

JVM執行main方法的過程:

  1.為main方法建立棧幀:

  JVM解析main方法,發現其 區域性變數的數量為 2,運算元棧的數量為1, 則會為main方法建立一個棧幀(VM Stack),並將其加入虛擬機器棧中:

  

  2. 完成棧幀初始化:

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

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

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

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

    

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

機器指令 組合語言 解釋 對棧幀的影響
0x12 0x10 ldc #16 將常量池中第16個常量池項引用推到運算元棧棧頂。
常量池第16項是CONSTANT_UTF-8_INFO項,表示”Louis”字串

0x4c astore_1

運算元棧的棧頂元素出棧,將棧頂元素的值賦給index=1 的區域性變量表元素上。

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

0x2b aload_1 將區域性變量表中index=1的元素的值推到運算元棧棧頂

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方法的呼叫,具體棧幀資訊如下:

  

  具體的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."<init>":(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指令時,腦海中對棧幀有個清晰的認識:

  

4. 機器指令的格式

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

  

  注:

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

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

5. 機器指令的執行模式---基於運算元棧的模式

  對於傳統的物理機而言,大部分的機器指令的設計都是暫存器的,物理機內設定若干個暫存器,用以儲存機器指令執行過程中的值,暫存器的數量和支援的指令的個數決定了這個機器的處理能力。

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

  

  本文旨在介紹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