關注文末公眾號,有驚喜福利!
Java代碼有很多種不同的運行方式,比如說可以在開發工具中運行,可以雙擊執行jar檔案運行,也可以在命令列中運行,甚至可以在網頁,
這些執行方式都離不開JRE,Java運行時環境,
JRE僅包含運行Java程式的必需組件,包括Java虛擬機以及Java核心類別庫等,我們Java程式員經常接觸到的JDK(Java開發工具包)同樣包含了JRE,并且還附帶了一系列開發、診斷工具,
然而,運行C++代碼則無需額外的運行時,往往把這些代碼直接編譯成CPU所能理解的代碼格式,即機器碼,
比如下圖的中間列,就是用C語言寫的Helloworld程式的編譯結果,
C程式編譯而成的機器碼就是一個個的位元組,它們是給機器讀的,那為讓開發人員也能理解,用反匯編器將其轉換成匯編代碼(如下圖的最右列所示),
; 最左列是偏移;中間列是給機器讀的機器碼;最右列是給人讀的匯編代碼
0x00: 55 push rbp
0x01: 48 89 e5 mov rbp,rsp
0x04: 48 83 ec 10 sub rsp,0x10
0x08: 48 8d 3d 3b 00 00 00 lea rdi,[rip+0x3b]
; 加載"Hello, World!\n"
0x0f: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
0x16: b0 00 mov al,0x0
0x18: e8 0d 00 00 00 call 0x12
; 呼叫printf方法
0x1d: 31 c9 xor ecx,ecx
0x1f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
0x22: 89 c8 mov eax,ecx
0x24: 48 83 c4 10 add rsp,0x10
0x28: 5d pop rbp
0x29: c3 ret
為什么Java要在虛擬機里運行?
Java作為一門高級程式語言,它的語法非常復雜,抽象程度也很高,因此,直接在硬體上運行這種復雜的程式并不現實,所以呢,在運行Java程式之前,需要對其進行一番轉換,
轉換怎么操作
設計一個面向Java語言特性的虛擬機,并通過編譯器將Java程式轉換成該虛擬機所能識別的指令序列,即Java位元組碼,
之所以這么取名,是因為Java位元組碼指令的操作碼(opcode)被固定為一個位元組,
下圖的中間列,正是用Java寫的Helloworld程式編譯而成的位元組碼,可以看到,它與C版本的編譯結果一樣,都是由一個個位元組組成的,
同樣可以將其反匯編為人類可讀的代碼格式(如下圖的最右列所示),
Java版本的編譯結果相對精簡一些,Java虛擬機相對于物理機而言,抽象程度更高,
# 最左列是偏移;中間列是給虛擬機讀的機器碼;最右列是給人讀的代碼
0x00: b2 00 02 getstatic java.lang.System.out
0x03: 12 03 ldc "Hello, World!"
0x05: b6 00 04 invokevirtual java.io.PrintStream.println
0x08: b1 return
Java虛擬機常見的是在各個現有平臺(如Windows_x64、Linux_aarch64)上提供軟體實作,一旦一個程式被轉換成Java位元組碼,便可在不同平臺上的虛擬機實作里運行,即“一次撰寫,到處運行”,
虛擬機的另外一個好處是它帶來托管環境(Managed Runtime),代替我們處理一些代碼中冗長而且容易出錯的部分,自動記憶體管理與垃圾回收,這部分內容甚至催生了一波垃圾回收調優,
托管環境還提供了諸如陣列越界、動態型別、安全權限等等的動態檢測,
Java虛擬機具體是怎樣運行Java位元組碼的?
以標準JDK中的HotSpot虛擬機為例,從虛擬機以及底層硬體兩個角度,給你講一講Java虛擬機具體是怎么運行Java位元組碼的,
虛擬機視角,執行Java代碼首先要將它編譯而成的class檔案加載到Java虛擬機,
加載后的Java類會被存放于方法區(Method Area),實際運行時,虛擬機會執行方法區內的代碼,
這和段式記憶體管理中的代碼段類似,而且,Java虛擬機同樣也在記憶體中劃分出堆和堆疊來存盤運行時資料,
但Java虛擬機會將堆疊細分為面向Java方法的Java方法堆疊,面向本地方法(用C++寫的native方法)的本地方法堆疊,以及存放各個執行緒執行位置的PC暫存器,
運行程序中,每當呼叫進入一個Java方法,Java虛擬機會在當前執行緒的Java方法堆疊中生成一個堆疊幀,存放區域變數以及位元組碼的運算元,這個堆疊幀的大小是提前計算好的,而且Java虛擬機不要求堆疊幀在記憶體空間里連續分布,
當退出當前執行的方法時,不管是正常回傳、例外回傳,Java虛擬機均會彈出當前執行緒的當前堆疊幀,并舍棄,
硬體視角,Java位元組碼無法直接執行,因此,Java虛擬機需要將位元組碼翻譯成機器碼,
HotSpot翻譯程序有兩種形式:
- 解釋執行,逐條將位元組碼翻譯成機器碼并執行
無需等待編譯 - 即時編譯(Just-In-Time compilation,JIT),將一個方法中包含的所有位元組碼編譯成機器碼后再執行
實際運行速度更快

HotSpot默認采用混合模式,綜合了解釋執行和即時編譯兩者的優點:
先解釋執行位元組碼,而后將其中反復執行的熱點代碼,以方法為單位進行即時編譯,
Java虛擬機的運行效率
HotSpot采用了多種技術來提升啟動性能以及峰值性能,即時編譯便是其中最重要的技術之一,
即時編譯建立在程式符合二八定律,百分之二十的代碼占據了百分之八十的計算資源,
- 對占據大部分的不常用的代碼,無需耗費時間將其編譯成機器碼,而是采取解釋執行的方式運行
- 對于僅占據小部分的熱點代碼,我們則可以將其編譯成機器碼,以達到理想運行速度,
理論即時編譯后的Java程式的執行效率,是可能超過C++,因為與靜態編譯相比,即時編譯擁有程式的運行時資訊,并且能夠根據這個資訊做出相應的優化,
虛方法是用來實作多型性,對一個虛方法呼叫,盡管有很多目標方法,但實際運行程序中,可能只呼叫其中一個,
這資訊可被即時編譯器所利用,規避虛方法呼叫的開銷,達到比靜態編譯的C++程式更高的性能,
為滿足不同用戶場景的需要,HotSpot內置了多個即時編譯器:C1、C2和Graal,
Graal是Java 10正式引入的實驗性即時編譯器,
之所以引入多個即時編譯器,為在編譯時間和生成代碼的執行效率之間進行取舍,C1又叫做Client編譯器,面向對啟動性能有要求的客戶端GUI程式,采用的優化手段相對簡單,因此編譯時間較短,
C2又叫做Server編譯器,面向對峰值性能有要求的服務器端程式,采用的優化手段相對復雜,因此編譯時間較長,但同時生成代碼的執行效率較高,
從Java 7開始,HotSpot默認采用分層編譯的方式:熱點方法首先會被C1編譯,而后熱點方法中的熱點會進一步被C2編譯,
為了不干擾應用的正常運行,HotSpot的即時編譯是放在額外的編譯執行緒中進行的,HotSpot會根據CPU的數量設定編譯執行緒的數目,并且按1:2的比例配置給C1及C2編譯器,
在計算資源充足的情況下,位元組碼的解釋執行和即時編譯可同時進行,編譯完成后的機器碼會在下次呼叫該方法時啟用,以替換原本的解釋執行,
總結
在虛擬機中運行,是因為它提供了可移植性,一旦Java代碼被編譯為Java位元組碼,便可以在不同平臺上的Java虛擬機實作上運行,此外,虛擬機還提供了一個代碼托管的環境,代替我們處理部分冗長而且容易出錯的事務,例如記憶體管理,
Java虛擬機將運行時記憶體區域劃分為五個部分,分別為方法區、堆、PC暫存器、Java方法堆疊和本地方法堆疊,Java程式編譯而成的class檔案,需要先加載至方法區中,方能在Java虛擬機中運行,
為了提高運行效率,標準JDK中的HotSpot虛擬機采用的是一種混合執行的策略,
它會解釋執行Java位元組碼,然后會將其中反復執行的熱點代碼,以方法為單位進行即時編譯,翻譯成機器碼后直接運行在底層硬體之上,
HotSpot裝載了多個不同的即時編譯器,以便在編譯時間和生成代碼的執行效率之間做取舍,
參考
- [1]https://en.wikipedia.org/wiki/Java_processor
- [2]https://wiki.openjdk.java.net/display/CodeTools/asmtools
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/389047.html
標籤:其他
上一篇:nginx從入門到實踐-基礎篇
下一篇:AWS 首席技術專家離職后加盟 MongoDB;Docker hub 發生中斷;MLSQL 正式更名 Byzer,打造新一代開源語言生態 | 開源日報
