Java虛擬機和真實的計算機同樣,運行的都是二進制的機器碼;而咱們將.java 源代碼編譯成.class 文件,class文件即是Java虛擬機可以認識的二進制機器碼,Java可以識別class文件中的信息和機器指令,進而執行這些機器指令。那麼,Java虛擬機是如何運行這些二進制的機器碼的呢? 本文將經過一個很是簡單的例子,帶你感覺一下Java虛擬機運行機器碼的過程和其工做的基本原理。css
Java虛擬機在運行時會爲每個線程在內存中分配了一個虛擬機棧,來表示線程的運行狀態和信息,虛擬機棧中的元素稱之爲棧幀(JVM stack frame),每個棧幀表示這對一個方法的調用信息。以下所示:ide
上述的描述可能會有點抽象,爲了給讀者一個直觀的感覺,咱們定義一個簡單的Java類,而後執行這個運行這個類,逐步分析整個Java虛擬機的運行時信息的組織的。學習
咱們將定義以下帶有main方法的簡單類org.louis.jvm.codeset.Bootstrap.java ,逐步分析該類在JVM中是如何表示的,方法是如何一步步運行的:ui
package org.louis.jvm.codeset; /** * JVM 原理簡單用例 * @author louis * */ public class Bootstrap { public static void main(String[] args) { String name = "Louis"; greeting(name); } public static void greeting(String name) { System.out.println( "Hello,"+name); } }
當咱們將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 的引用值初始化到局部點亮表中;
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/StringBuilder59 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;)V2a 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 結束返回
通常地,對於java方法的執行,在JVM在其某一特定線程的虛擬機棧(JVM Stack) 中會爲方法分配一個 局部變量表,一個操做數棧,用以存儲方法的運行過程當中的中間值存儲。
因爲JVM的指令是基於棧的,即大部分的指令的執行,都伴隨着操做數的出棧和入棧。因此在學習JVM的機器指令的時候,必定要銘記一點:
每一個機器指令的執行,對操做數棧和局部變量的影響,充分地瞭解了這個機制,你就能夠很是順暢地讀懂class文件中的二進制機器指令了。
以下是棧幀信息的簡化圖,在分析JVM指令時,腦海中對棧幀有個清晰的認識:
所謂的機器指令,就是隻有機器纔可以認識的二進制代碼。一個機器指令分爲兩部分組成:
注:
a). 如上圖所示JVM虛擬機的操做碼是由一個字節組成的,也就是說對於JVM虛擬機而言,其指令的數量最多爲 2^8,即 256個;
b). 上圖中的操做碼如:b2,bb,59....等等都是表示某一特定的機器指令,爲了方便咱們識別,其分別有相應的助記符:getstatic,new,dup.... 這樣方便咱們理解。
可是Java虛擬機的設計的機制並非這樣的,Java虛擬機使用操做數棧 來存儲機器指令的運算過程當中的值。全部的操做數的操做,都要遵循出棧和入棧的規則,因此在《Java虛擬機規範》中,你會發現有不少機器指令都是關於出棧入棧的操做。