Java代碼是怎么運行的


 

前言....

作為一名 Java 程序員,你應該知道,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 語言特性的虛擬機,並通過編譯器將 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 虛擬機可以由硬件實現 [1],但更為常見的是在各個現有平台(如 Windows_x64、Linux_aarch64)上提供軟件實現。這么做的意義在於,一旦一個程序被轉換成 Java 字節碼,那么它便可以在不同平台上的虛擬機實現里運行。這也就是我們經常說的“一次編寫,到處運行”。

虛擬機的另外一個好處是它帶來了一個托管環境(Managed Runtime)。這個托管環境能夠代替我們處理一些代碼中冗長而且容易出錯的部分。其中最廣為人知的當屬自動內存管理與垃圾回收,這部分內容甚至催生了一波垃圾回收調優的業務。

除此之外,托管環境還提供了諸如數組越界、動態類型、安全權限等等的動態檢測,使我們免於書寫這些無關業務邏輯的代碼。

Java 虛擬機具體是怎樣運行 Java 字節碼的?

下面我將以標准 JDK 中的 HotSpot 虛擬機為例,從虛擬機以及底層硬件兩個角度,給你講一講 Java 虛擬機具體是怎么運行 Java 字節碼的。

從虛擬機視角來看,執行 Java 代碼首先需要將它編譯而成的 class 文件加載到 Java 虛擬機中。加載后的 Java 類會被存放於方法區(Method Area)中。實際運行時,虛擬機會執行方法區內的代碼。

如果你熟悉 X86 的話,你會發現這和段式內存管理中的代碼段類似。而且,Java 虛擬機同樣也在內存中划分出堆和棧來存儲運行時數據。

不同的是,Java 虛擬機會將棧細分為面向 Java 方法的 Java 方法棧,面向本地方法(用 C++ 寫的 native 方法)的本地方法棧,以及存放各個線程執行位置的 PC 寄存器。

                                                        

 

在運行過程中,每當調用進入一個 Java 方法,Java 虛擬機會在當前線程的 Java 方法棧中生成一個棧幀,用以存放局部變量以及字節碼的操作數。這個棧幀的大小是提前計算好的,而且 Java 虛擬機不要求棧幀在內存空間里連續分布。

當退出當前執行的方法時,不管是正常返回還是異常返回,Java 虛擬機均會彈出當前線程的當前棧幀,並將之舍棄。

從硬件視角來看,Java 字節碼無法直接執行。因此,Java 虛擬機需要將字節碼翻譯成機器碼。

在 HotSpot 里面,上述翻譯過程有兩種形式:第一種是解釋執行,即逐條將字節碼翻譯成機器碼並執行;第二種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有字節碼編譯成機器碼后再執行。

 

                                                

 

 

前者的優勢在於無需等待編譯,而后者的優勢在於實際運行速度更快。HotSpot 默認采用混合模式,綜合了解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而后將其中反復執行的熱點代碼,以方法為單位進行即時編譯。

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 虛擬機實現上運行。此外,虛擬機還提供了一個代碼托管的環境,代替我們處理部分冗長而且容易出錯的事務,例如內存管理。

Java 虛擬機將運行時內存區域划分為五個部分,分別為方法區、堆、PC 寄存器、Java 方法棧和本地方法棧。Java 程序編譯而成的 class 文件,需要先加載至方法區中,方能在 Java 虛擬機中運行。

為了提高運行效率,標准 JDK 中的 HotSpot 虛擬機采用的是一種混合執行的策略。

它會解釋執行 Java 字節碼,然后會將其中反復執行的熱點代碼,以方法為單位進行即時編譯,翻譯成機器碼后直接運行在底層硬件之上。

HotSpot 裝載了多個不同的即時編譯器,以便在編譯時間和生成代碼的執行效率之間做取舍。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM