《Java虛擬機原理圖解》8.JVM機器指令集

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接和本聲明。
本文連接: http://www.javashuo.com/article/p-njuuyopt-cw.html


0. 前言

     Java虛擬機和真實的計算機同樣,運行的都是二進制的機器碼;而咱們將.java 源代碼編譯成.class 文件,class文件即是Java虛擬機可以認識的二進制機器碼,Java可以識別class文件中的信息和機器指令,進而執行這些機器指令。那麼,Java虛擬機是如何運行這些二進制的機器碼的呢? 本文將經過一個很是簡單的例子,帶你感覺一下Java虛擬機運行機器碼的過程和其工做的基本原理。css


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

一、Java虛擬機對運行時虛擬機棧(JVM Stack) 的組織html

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

三、JVM對一個方法執行的基本策略markdown

4. JVM機器指令的格式app

5. 機器指令的執行模式---基於操做數棧的模式
jvm


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

    Java虛擬機在運行時會爲每個線程在內存中分配了一個虛擬機棧,來表示線程的運行狀態和信息,虛擬機棧中的元素稱之爲棧幀(JVM stack frame),每個棧幀表示這對一個方法的調用信息。以下所示:ide



上述的描述可能會有點抽象,爲了給讀者一個直觀的感覺,咱們定義一個簡單的Java類,而後執行這個運行這個類,逐步分析整個Java虛擬機的運行時信息的組織的。學習

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


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

    
    
    
    
    
  1. package org.louis.jvm.codeset;
  2. /**
  3. * JVM 原理簡單用例
  4. * @author louis
  5. *
  6. */
  7. public class Bootstrap {
  8. public static void main(String[] args) {
  9. String name = "Louis";
  10. greeting(name);
  11. }
  12. public static void greeting(String name)
  13. {
  14. System.out.println( "Hello,"+name);
  15. }
  16. }


當咱們將Bootstrap.java 編譯成Bootstrap.class 並運行這段程序的時候,在JVM複雜的運行邏輯中,會有如下幾步:lua

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.五、 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 的引用值初始化到局部點亮表中;



    1. 接着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   aload_0 將第一個局部變量的引用推到棧頂。
當前局部變量表的第一個局部變量引用是 :「Louis」,即將Louis推到棧頂
 
b6 20 26 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虛擬機規範》中,你會發現有不少機器指令都是關於出棧入棧的操做。

相關文章
相關標籤/搜索