Java之因此號稱「一次編譯,處處運行」,主要緣由是JVM屏蔽了各個計算機平臺相關的軟件(大多指系統)或者硬件之間的差別,使得與平臺相關的耦合統一由JVM提供者來實現。在本文,筆者將與你們概覽JVM的體系結構與工做方式。java
JVM和實體機器的體系結構有點類似,主要由如下幾個部分組成:linux
下面簡單介紹一下android
執行引擎是JVM的核心部分,執行引擎的做用就是解析JVM字節碼指令,獲得執行結果。JVM虛擬機規範
詳細地定義了執行引擎遇到每條字節碼指令時應該處理什麼,而且應該獲得什麼結果。可是並無規定執行引擎應該如何或採用什麼方式處理而獲得這個結果。由於執行引擎具體採起什麼方式由JVM的實現廠家本身去實現,是直接解釋執行仍是採用JIT轉換成本地代碼去執行,仍是採用寄存器這個芯片模式去執行均可以。因此執行引擎的具體實現有很大的發揮空間,如SUN的hotspot的基於棧的執行引擎,而Google的Dalvik的基於寄存器的執行引擎。編程
執行引擎也就是執行一條條代碼的一個流程,而代碼都是包含在方法體內的,因此執行引擎本質上就是執行一個個方法所串起來的流程,對應到操做系統中一個執行流程是一個Java進程仍是一個Java線程呢?很顯然是後者,由於一個Java進程能夠有多個同時執行的執行流程。這樣說來每一個Java線程就是一個執行引擎的實例,那麼在一個JVM實例中就會同時有多個執行在引擎在工做,這些執行引擎有的在執行用戶的程序,有的在執行JVM內部的程序(如Java垃圾收集器)。segmentfault
執行引擎在執行一段程序時須要存儲一些東西,如:操做碼須要的操做數,操做碼執行結果須要保存。Class類的字節碼還有類的對象等信息都須要在執行引擎執行以前就準備好。一個JVM實例會有一個方法區、Java堆、Java棧、PC寄存器和本地方法區。其中方法區和Java堆是全部線程共享的,也就是能夠被全部執行引擎實例訪問。每一個新的執行引擎實例被建立時會爲這個執行引擎建立一個Java棧和一個PC寄存器,若是當前正在執行一個Java方法,那麼在當前的這個Java棧中保存的是該線程中方法調用的狀態,包括方法的參數、方法的局部變量、方法的返回值以及運算的中間結果等。而PC寄存器會指向即將執行的下一個指令。數據結構
若是是本地方法調用,則存儲在本地方法調用棧中或者特定實現中的某個內存區域中。架構
考慮到篇幅大小,故另寫一篇文章:淺析JVM以內存管理性能
以前簡單分析了JVM的基本結構,下面再簡單分析一下JVM是如何執行字節命令的,也就是前面介紹的執行引擎是如何工做的。google
咱們知道,計算機只接受機器指令,其餘高級語言必須先通過編譯器編譯成機器指令才能被計算機正確執行。然而機器語言通常和硬件平臺密切相關(指令集、CPU架構的因素等),但高級語言會屏蔽全部底層硬件平臺甚至軟件平臺。之因此能夠屏蔽是由於中間有個編譯環節,與硬件耦合的麻煩就交給了編譯器。因此,想說的是:編譯器和操做系統的關係很是密切。好比C語言在win下編譯器爲Microsoft C,而Linux下一般是gcc。spa
一般一個程序從編寫到執行會經歷如下階段:
除了一、7兩步,其餘都是由現代意義上的編譯器統一完成的。最多見的栗子是在Linux平臺下咱們一般安裝一個軟件須要通過configure、make、make install、make clean這4個步驟來完成。
值得注意的是,咱們一般所說的是編譯器都是將某種高級語言直接編譯成可執行的目標機器語言(實際上在某種操做系統中是ixuyao動態鏈接的二進制文件:在Windows下是dynamic link library,Dll;在linux下是Shared library,SO庫)。可是實際上還有一些編譯是將一種高級語言編譯成另外一種高級語言,或者將低級語言編譯成高級語言(反編譯),或者將高級語言編譯成虛擬機目標語言,如Java編譯器等。
再回到如何讓機器(不論是實體機仍是虛擬機)執行代碼的主題,不論是如何指令集都只有集中最基本的元素:加、減、乘、除、求餘、求模等。這些運算又能夠進一步分解成二進制位運算:與、或、異或等。這些運算又經過指令來完成,而指令的核心目的就是肯定須要運算的種類(操做碼)和運算須要的數據(操做數),以及從哪裏(寄存器或棧)獲取操做數、將運算結果存放到什麼地方(寄存器或是棧)等。這種不一樣的操做方式又將指令劃分紅:一地址指令、二地址指令、三地址指令和零地址指令等n地址指令。相應的指令集會有對應的架構實現,如基於寄存器的架構實現或基於棧的架構實現,這裏的基於寄存器或棧都是指在一個指令中的操做數是如何存取的。
學過數據結構的小夥伴都知道,對棧進行操做是要先將全部的操做數壓入棧,而後根據指令中操做碼選擇必定的元素彈出計算後再壓入棧。相對於寄存器操做(將兩個操做數存入寄存器後進行加法運算後再將加過存入其中一個寄存器便可)是比較麻煩的。那麼,JVM爲何還要基於棧來設計呢?
JVM要設計成與平臺無關
每當建立一個新的線程時,JVM會爲這個線程建立一個Java棧,同時會爲這個線程分配一個PC寄存器,而且這個PC寄存器會指向這個線程的第一行可執行代碼。每當調用一個新方法時會在這個棧上建立一個新的棧幀數據結構,這個幀棧會保留這個方法的一些元信息——如這個方法中定義的局部變量、一些用來支持常量池的解析、正常方法返回及異常處理機制等。
JVM調用某些指令時可能須要使用到常量池中的一些常量,或者是獲取常量表明的數據或者這個數據指向的實例化對象,而這些信息都存儲在全部線程共享的方法區和Java堆中。
下面以一個簡單的程序來講明執行引擎的執行過程。
public class Math{ public static void main(String[]args){ int a=1; int b=2; int c = (a+b)*10; } }
其中對應的字節碼指令以下:
偏移量 | 指令 | 說明 |
---|---|---|
0 | iconst_1 | 常數1入棧 |
1 | istore_1 | 將棧頂元素移入本地變量1存儲 |
2 | iconst_1 | 常數2入棧 |
3 | istore_2 | 將棧頂元素移入本地變量1存儲 |
4 | iload_1 | 本地變量1入棧 |
5 | iload_2 | 本地變量2入棧 |
6 | iadd | 彈出棧頂兩個元素相加 |
7 | bipush 10 | 將10入棧 |
9 | imul | 棧頂兩個元素相乘 |
10 | istore_3 | 棧頂元素移入本地變量3存儲 |
11 | return | 返回 |
對應到執行引擎的各執行部件如圖
在開始執行方法以前,PC寄存器存儲的指針是第1條指令的地址,局部變量區和操做棧都沒有數據。從第1條和第4條指令分別將a、b兩個本地變量賦值,對應到局部變量區就是1和2分別存儲常數1和2。
前4條指令執行完後,PC寄存器當前指向的是下一條指令地址,也就是第5條指令,這時局部變量區已經保存了兩個局部變量(也就是變量a和變量b的值),而操做棧裏仍然沒有值,由於兩次常數入棧後又分別出棧了。
將第5條和第6條指令分別是將兩個局部變量入棧,而後相加。如圖
1先入棧2後入棧,棧頂元素是2,第7條指令是棧頂的兩個元素彈出後相加,將結果再入棧,這時整個部件狀態如圖
能夠看出,變量a和變量b想加的結果3存在當前棧的棧頂中,接下來是第8條指令將10入棧,如圖
當前PC寄存器執行的地址是9,下一個操做是將當前棧的兩個操做數彈出進行相乘並把結果壓入棧,如圖
第10條指令是將當前的棧頂元素存入局部變量3中,這是狀態如圖
第10條指令執行完後棧中元素出棧,出棧的元素存儲在局部變量區3中,對應的是變量c的值。最後一條指令是return ,這條指令執行完後當前的這個方法對應的這些部件會被JVM回收,局部變量區的全部值將所有釋放,PC寄存器會被銷魂,在Java棧中與這個方法對應的棧幀將消失。
JVM的方法調用分別爲兩種:
因爲本地方法調用各個虛擬機的實現不太相同,因此這裏主要介紹Java的方法調用狀況。
public class Math{ public static void main(String[]args){ int a =1; int b=2; int c=math(a,b)/10; } public static int math(int a, int b){ return (a+b)*10; } }
那麼其中兩個方法對應的字節碼分別以下:
public static void main(java.lang.String[]); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: invokestatic #2; //Method math:(II) 9: bipush 10 11: idiv 12: istore_3 13: return public static int math(int ,int ); Code: 0: iload_0 1: iload_1 2: iadd 3: bipush 10 5: imul 6: ireturn
當JVM執行main方法時,首先將常數1和2分別存儲到局部變量區1和2中,而後調用靜態math方法。從math的字節碼指令能夠看出,math方法的兩個參數也存儲在其對應的方法棧幀中的局部變量區0和1中,先將這兩個局部變量分別入棧,而後進行相加操做再和常數10相乘。
那麼來看一下實際的操做,如圖
上圖是JVM執行到第5條指令時,執行引擎各部件的狀態圖,PC寄存器指向的是下一條執行math方法的地址。當執行invokestatic指令時JVM會爲math方法建立一個新的棧幀,而且將兩個參數存在math方法的棧幀的前兩個局部變量區中,這時PC寄存器會清零,而且會指向math方法對應棧幀的第一條指令地址,這時的狀態以下圖
執行invokestatic指令時,建立了一個新的棧幀,這是棧幀的局部變量中已經有了兩個變量了,這兩個變量是從main方法的棧幀中的操做棧中傳過來的。當執行math方法時,math方法對應的棧幀成爲當前的活動棧幀,PC寄存器保存的是當前這個戰爭中的下一條指令地址,因此是0。
math方法先將a、b兩個變量相加,再乘以10,最後返回這個結果執行到第5條指令的狀態,以下圖
math的操做棧中的棧頂元素相乘的結果是30,最後一條指令是ireturn,這條指令是將當前棧幀中的棧頂元素返回到調用這個方法的棧中,而這個棧幀也將撤銷,PC寄存器的值回覆調用棧的下一條指令地址,以下圖
main方法將math方法返回的結果再除以10存放在變量區3中,這時的狀態如圖所示
當執行return指令時main方法對應的棧幀也將撤銷,若是當前線程對應的Java棧中沒有棧幀,這個Java棧也將被JVM撤銷,整個JVM退出。