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