更新時間:2019-09-14 09:00:00 來源:動力節點 瀏覽2613次
作為一名Java程序員,你應該知道,Java代碼有很多種不同的運行方式。比如說可以在開發工具中運行,可以雙擊執行jar文件運行,也可以在命令行中運行,甚至可以在網頁中運行。當然,這些執行方式都離不開JRE,也就是Java運行時環境。實際上,JRE僅包含運行Java程序的必需組件,包括Java虛擬機以及Java核心類庫等。我們Java程序員經常接觸到的JDK(Java開發工具包)同樣包含了JRE,并且還附帶了一系列開發、診斷工具。然而,運行C++代碼則無需額外的運行時。我們往往把這些代碼直接編譯成CPU所能理解的代碼格式,也就是機器碼。
既然C++的運行方式如此成熟,那么你有沒有想過,為什么Java要在虛擬機中運行呢,Java虛擬機具體又是怎樣運行Java代碼的呢,它的運行效率又如何呢?
為什么Java要在虛擬機里運行?
Java作為一門高級程序語言,它的語法非常復雜,抽象程度也很高。因此,直接在硬件上運行這種復雜的程序并不現實。所以呢,在運行Java程序之前,我們需要對其進行一番轉換。
這個轉換具體是怎么操作的呢?當前的主流思路是這樣子的,設計一個面向Java語言特性的虛擬機,并通過編譯器將Java程序轉換成該虛擬機所能識別的指令序列,也稱Java字節碼。這里順便說一句,之所以這么取名,是因為Java字節碼指令的操作碼(opcode)被固定為一個字節。
舉例來說,下圖的中間列,正是用Java寫的Helloworld程序編譯而成的字節碼。可以看到,它與C版本的編譯結果一樣,都是由一個個字節組成的。
并且,我們同樣可以將其反匯編為人類可讀的代碼格式(如下圖的最右列所示)。不同的是,Java版本的編譯結果相對精簡一些。這是因為Java虛擬機相對于物理機而言,抽象程度更高。
#最左列是偏移;中間列是給虛擬機讀的機器碼;最右列是給人讀的代碼
0x00;b20002;getstaticjava.lang.System.out
0x03;1203;ldc"Hello,World!"
0x05;b60004;invokevirtualjava.io.PrintStream.println
0x08;b1;return
Java虛擬機可以由硬件實現,但更為常見的是在各個現有平臺(如Windows、Linux)上提供軟件實現。這么做的意義在于,一旦一個程序被轉換成Java字節碼,那么它便可以在不同平臺上的虛擬機實現里運行。這也就是我們經常說的“一次編寫,到處運行”。
虛擬機的另外一個好處是它帶來了一個托管環境(ManagedRuntime)。這個托管環境能夠代替我們處理一些代碼中冗長而且容易出錯的部分。其中最廣為人知的當屬自動內存管理與垃圾回收,這部分內容甚至催生了一波垃圾回收調優的業務。
除此之外,托管環境還提供了諸如數組越界、動態類型、安全權限等等的動態檢測,使我們免于書寫這些無關業務邏輯的代碼。
Java虛擬機具體是怎樣運行Java字節碼的?
下面以標準JDK中的HotSpot虛擬機為例,從虛擬機以及底層硬件兩個角度,大概講一講Java虛擬機具體是怎么運行Java字節碼的。
從虛擬機視角來看,執行Java代碼首先需要將它編譯而成的class文件加載到Java虛擬機中。加載后的Java類會被存放于方法區(MethodArea)中。實際運行時,虛擬機會執行方法區內的代碼。
Java虛擬機在內存中劃分出堆和棧來存儲運行時數據,虛擬機會將棧細分為面向Java方法的Java方法棧,面向本地方法(用C++寫的native方法)的本地方法棧,以及存放各個線程執行位置的PC寄存器。
在運行過程中,每當調用進入一個Java方法,Java虛擬機會在當前線程的Java方法棧中生成一個棧幀,用以存放局部變量以及字節碼的操作數。這個棧幀的大小是提前計算好的,而且Java虛擬機不要求棧幀在內存空間里連續分布。
當退出當前執行的方法時,不管是正常返回還是異常返回,Java虛擬機均會彈出當前線程的當前棧幀,并將之舍棄。
從硬件視角來看,Java字節碼無法直接執行。因此,Java虛擬機需要將字節碼翻譯成機器碼。
在HotSpot里面,上述翻譯過程有兩種形式:第一種是解釋執行,即逐條將字節碼翻譯成機器碼并執行;第二種是即時編譯(Just-In-Timecompilation,JIT),即將一個方法中包含的所有字節碼編譯成機器碼后再執行。
前者的優勢在于無需等待編譯,而后者的優勢在于實際運行速度更快。HotSpot默認采用混合模式,綜合了解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而后將其中反復執行的熱點代碼,以方法為單位進行即時編譯。
Java虛擬機的運行效率究竟是怎樣的?
HotSpot采用了多種技術來提升啟動性能以及峰值性能,剛剛提到的即時編譯便是其中最重要的技術之一。
即時編譯建立在程序符合二八定律的假設上,也就是百分之二十的代碼占據了百分之八十的計算資源。
對于占據大部分的不常用的代碼,我們無需耗費時間將其編譯成機器碼,而是采取解釋執行的方式運行;另一方面,對于僅占據小部分的熱點代碼,我們則可以將其編譯成機器碼,以達到理想的運行速度。
理論上講,即時編譯后的Java程序的執行效率,是可能超過C++程序的。這是因為與靜態編譯相比,即時編譯擁有程序的運行時信息,并且能夠根據這個信息做出相應的優化。
舉個例子,我們知道虛方法是用來實現面向對象語言多態性的。對于一個虛方法調用,盡管它有很多個目標方法,但在實際運行過程中它可能只調用其中的一個。
這個信息便可以被即時編譯器所利用,來規避虛方法調用的開銷,從而達到比靜態編譯的C++程序更高的性能。
為了滿足不同用戶場景的需要,HotSpot內置了多個即時編譯器:C1、C2和Graal。Graal是Java10正式引入的實驗性即時編譯器,之所以引入多個即時編譯器,是為了在編譯時間和生成代碼的執行效率之間進行取舍。C1又叫做Client編譯器,面向的是對啟動性能有要求的客戶端GUI程序,采用的優化手段相對簡單,因此編譯時間較短。
C2又叫做Server編譯器,面向的是對峰值性能有要求的服務器端程序,采用的優化手段相對復雜,因此編譯時間較長,但同時生成代碼的執行效率較高。
從Java7開始,HotSpot默認采用分層編譯的方式:熱點方法首先會被C1編譯,而后熱點方法中的熱點會進一步被C2編譯。
為了不干擾應用的正常運行,HotSpot的即時編譯是放在額外的編譯線程中進行的。HotSpot會根據CPU的數量設置編譯線程的數目,并且按1:2的比例配置給C1及C2編譯器。
在計算資源充足的情況下,字節碼的解釋執行和即時編譯可同時進行。編譯完成后的機器碼會在下次調用該方法時啟用,以替換原本的解釋執行。
總結
Java代碼之所以要在虛擬機中運行,是因為它提供了可移植性。一旦Java代碼被編譯為Java字節碼,便可以在不同平臺上的Java虛擬機實現上運行。此外,虛擬機還提供了一個代碼托管的環境,代替我們處理部分冗長而且容易出錯的事務,例如內存管理。
Java虛擬機將運行時內存區域劃分為五個部分,分別為方法區、堆、PC寄存器、Java方法棧和本地方法棧。Java程序編譯而成的class文件,需要先加載至方法區中,方能在Java虛擬機中運行。
為了提高運行效率,標準JDK中的HotSpot虛擬機采用的是一種混合執行的策略。
它會解釋執行Java字節碼,然后會將其中反復執行的熱點代碼,以方法為單位進行即時編譯,翻譯成機器碼后直接運行在底層硬件之上。
HotSpot裝載了多個不同的即時編譯器,以便在編譯時間和生成代碼的執行效率之間做取舍。
以上就是動力Java培訓機構小編介紹的“JVM是如何運行Java代碼的”的內容,希望對大家有幫助,如有疑問,請在線咨詢,有專業老師隨時為你服務。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習