Java 從虛擬機層面看程序代碼是怎麼運行起來的

專欄原創出處:github-源筆記文件 github-源碼 ,歡迎 Star,轉載請附上原文出處鏈接和本聲明。

Java JVM-虛擬機專欄系列筆記,系統性學習可訪問個人覆盤筆記-技術博客 Java JVM-虛擬機

一、Java 源代碼怎麼執行的

許多 Java 虛擬機的執行引擎在執行 Java 代碼的時候都是解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼執行)混合運行。

大體流程爲:

  • 編寫 java 文件源碼
  • 通過 javac 編譯器將 java 源碼編譯爲字節碼流
  • 通過解釋器解釋執行字節碼
  • 隨着時間推移,即時編譯器 (JIT) 介入,把越來越多的字節碼編譯成本地代碼(機器碼)執行

本文中無特殊說明,編譯器指即時編譯器,即在運行期間的編譯。

二、解釋器是怎麼解釋字節碼流執行的

我們使用 javac 編譯器編譯完後會生成字節碼流,這些字節碼解釋執行方式有 2 種。一種是基於棧的指令集,一種是基於寄存器的指令集。

比如一個 1 + 1 的計算。

基於棧的指令集時:

iconst_1    將 1 放入棧頂
iconst_1    將 1 放入棧頂
iadd        將棧頂的 2 個數相加後結果放入棧頂
istore_0    將相加的結果放入局部變量表

基於寄存器的指令集時:

mov eax,1 把 EAX 寄存器的值設爲 1
add eax,1 再把這個值加 1 ,結果保存在了 EAX 寄存器

兩套指令集的優缺點:

  • 基於棧的指令集優點是可移植,因爲寄存器由硬件直接提供,受到硬件的約束。
  • 基於棧的指令集缺點理論上執行速度可能較慢,出棧入棧本身就涉及了大量的指令,而且棧是在內存中實現的。

實際中基於棧的指令集會被虛擬機優化,比如使用即時編譯,常用操作映射到寄存器。

三、編譯器是如何將字節碼編譯爲本地機器碼的

服務端編譯器和客戶端編譯器的編譯過程是有所差別。

對於客戶端編譯器來說:

它是一個相對簡單快速的三段式編譯器,主要的關注點在於局部性的優化,而放棄了許多耗時較長的全局優化手段。

在第一個階段,一個平臺獨立的前端將字節碼構造成一種高級中間代碼表示(High-Level Intermediate Representation,HIR,即與目標機器指令集無關的中間表示)。
HIR 使用靜態單分配(Static Single Assignment,SSA)的形式來代表代碼值,這可以使得一些在 HIR 的構造過程之中和之後進行的優化動作更容易實現。
在此之前編譯器已經會在字節碼上完成一部分基礎優化,如方法內聯、常量傳播等優化將會在字節碼被構造成 HIR 之前完成。

在第二個階段,一個平臺相關的後端從 HIR 中產生低級中間代碼表示(Low-Level Intermediate Representation,LIR,即與目標機器指令集相關的中間表示),
而在此之前會在 HIR 上完成另外一些優化,如空值檢查消除、範圍檢查消除等,以便讓 HIR 達到更高效的代碼表示形式。

最後的階段是在平臺相關的後端使用線性掃描算法(Linear Scan Register Allocation)在 LIR 上分配寄存器,並在 LIR 上做窺孔(Peephole)優化,然後產生機器代碼。

對於服務端編譯器來說:

服務端編譯器則是專門面向服務端的典型應用場景,併爲服務端的性能配置針對性調整過的編譯器,也是一個能容忍很高優化複雜度的高級編譯器,幾乎能達到 GNU C++ 編譯器使用-O2 參數時的優化強度。
它會執行大部分經典的優化動作,如:無用代碼消除、循環展開、循環表達式外提、消除公共子表達式、常量傳播、基本塊重排序等,
還會實施一些與 Java 語言特性密切相關的優化技術,如範圍檢查消除、空值檢查消除等。
另外,還可能根據解釋器或客戶端編譯器提供的性能監控信息,進行一些不穩定的預測性激進優化,如守護內聯、分支頻率預測等

服務端編譯採用的寄存器分配器是一個全局圖着色分配器,它可以充分利用某些處理器架構(如 RISC)上的大寄存器集合。
以即時編譯的標準來看,服務端編譯器無疑是比較緩慢的,但它的編譯速度依然遠遠超過傳統的靜態優化編譯器,
而且它相對於客戶端編譯器編譯輸出的代碼質量有很大提高,
可以大幅減少本地代碼的執行時間,從而抵消掉額外的編譯時間開銷,所以也有很多非服務端的應用選擇使用服務端模式的 HotSpot 虛擬機來運行。

四、爲什麼同時使用瞭解釋器與編譯器

解釋器與編譯器兩者各有優勢:

  • 當程序需要迅速啓動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即運行

  • 當程序啓動後,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼,這樣可以減少解釋器的中間損耗,獲得更高的執行效率

  • 當程序運行環境中內存資源限制較大,可以使用解釋執行節約內存(如部分嵌入式系統中和大部分的 JavaCard 應用中就只有解釋器的存在)

  • 當程序運行環境中內存資源限制較小,可以使用編譯執行來提升效率

五、提前編譯器

前面僅說明了即時編譯器,其實還有一種提前編譯器,在我們編譯字節碼時直接將部分字節碼生成本地代碼。

JDK 9 引入了用於支持對 Class 文件和模塊進行提前編譯的工具 Jaotc,以減少程序的啓動時間和到達全速性能的預熱時間,
但由於這項功能必須針對特定物理機器和目標虛擬機的運行參數來使用,加之限制太多,Java 開發人員對此瞭解、使用普遍比較少。

提前編譯器的兩條分支:

  • 做與傳統 C、C++ 編譯器類似的,在程序運行之前把程序代碼編譯成機器碼的靜態翻譯工作

  • 把原本即時編譯器在運行時要做的編譯工作提前做好並保存下來,下次運行到這些代碼(比如公共庫代碼在被同一臺機器其他 Java 進程使用)時直接把它加載進來使用

Android 安裝包如果提前編譯後,體積會變大。如果不提前編譯啓動運行可能會變慢。目前有一種優化手段就是空閒時間編譯。

六、即時編譯器的種類

JDK 10 前,HotSpot 擁有兩款即時編譯器,客戶端即時編譯器 C1。服務端即時編譯器 C2。

從 JDK 10 起,HotSpot 新增一個 Graal 目標是代替服務端即時編譯器 C2。

參考

專欄更多文章筆記

發佈了270 篇原創文章 · 獲贊 55 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章