Java筆記 — Java是如何執行的?

Carl
5 min readMay 25, 2018

對我個人而言, Java 是我最擅長的程式語言, 除了因為經歷過的工作幾乎都是以 Java 做為主要開發語言之外, 也是因為這是我在自學過程中花最多心力的語言. 從學生時代的為了考證照而讀, 到現在是為了讓自己成為更優秀的 Java developer 而持續進修, 一路上經過了各式各樣的洗禮, 見過了各種高手, 也覺得自己其實還是很弱小, 所以想開始寫這系列, 給自己做個紀錄.

對於Java, 我們常常會看到以下這些特色:

  • Write once, run anywhere. 優秀的跨平台能力是其早期的賣點之一.
  • GC, 即 Garbage Collection, Java 通過 GC 來回收與分配記憶體, 所以大部分情況下, 開發者不需要太過於關心記憶體的分配與回收
  • 是一種物件導向的程式語言
  • 強大的生態系支援, 看 Spring 全家桶那壓倒性的實力就可以知道了.

那麼, 回到本文的標題, Java 是如何執行的呢? 是直譯執行還是編譯執行呢? 這個問題其實滿籠統的, 或是說, 很開放, 因為其出發點是想要看看開發者的多方面知識是否足夠健全.

我們先來回想一下從開發到執行 Java 程式的過程: 作為 Java 開發者, 我們會先進行開發, 撰寫 Java source code, 然後通過 javac 指令進行編譯, 將源碼編譯為 bytecode. 再來, 到了 runtime, 透過 JVM 內嵌的直譯器將 bytecode 轉換為最後的 machine code. 這邊要額外提的一點是, 對於常見的 JVM, 如 Oracle JDK 的 Hotspot JVM, 提供了 JIT (Just -In-Time) 編譯器, 有時又稱為動態編譯器. JIT 可以在 runtime 時期用直譯的方式執行程式, 並且同時分析我們的程式碼, 從中找出比較常運行到的片段, 這些程式片段通常又稱為 hot spots (中文稱為熱點原始碼). 找出這些 hot spots 後, JIT 就可以將這些部分編譯成 machine code (這個過程是在獨立的執行緒中執行的, 基本上不會影響程式的執行). 我們都知道, 在程式中, 其實大多數的執行時間都只會花在少數的程式碼片段上, 如果可以把這些花比較多時間的程式給編譯成 machine code, 而不是每次都要用直譯的方式去執行的話, 基本上是有助於效能的提升的.

所以根據我們對 Java 的了解, 基本上我們可以把 Java 分為編譯時期 (compile time) 以及運行時期 (runtime). 當然, Java 的編譯與 C/C++ 是有著完全不同的意義的, 對 Java 來說, 編譯是指將 Java source code 轉換成 .class 檔案的部分, 且 .class 檔案裡面實際上都是 bytecode, 並不像 C/C++ 一樣是編譯成可以直接執行的 machine code. Java 通過 bytecode 和 JVM 這種跨平台的抽象機制, 屏蔽了作業系統與硬體的細節, 這就是所謂 “Write once, run anywhere.” 的技術基礎.

在運行時期, JVM 會通過 Class-Loader 來載入 bytecode, 然後看是要透過直譯還是編譯的方式去執行. 所以, 在現行的 Java 版本中, 如 JDK8, 其實際上應該是屬於混合的執行模式 (-Xmixed) — 即直譯與編譯混合. 若你對 JVM 略懂的話, 應該知道 JVM 常見的模式有兩種: server mode 以及 client mode. 通常運行在 server mode 之下的 JVM, 會運行上萬次 (預設剛好是10000) 的 invocation 以收集足夠的資訊來進行高效能的編譯, 在 client mode 下, 這個門檻(-XX:CompileThreshold)則大約是 1500次. 對 Oracle HotSpot JVM 來說, 其內建了兩個不同的 JIT compiler, C1 對應 client mode, 適用場景通常為對啟動速度有所要求的敏感應用程式, 如一般的 Java desktop application; C2 則對應到 sever mode, 其最佳化是專為長時間運作的 server side application 去設計的. 預設會採用所謂的分層編譯 (Tiered Compilation).

那麼, 對於編譯方式, 我們可以介入嗎? 其實是可以的, 除了預設的 -Xmixed 選項之外, 在 JVM 啟動後, 可以透過指定不同的參數對運行模式作切換:

  • -Xint: 跟 JVM 說只進行直譯式執行, 不要對原始碼進行編譯, 這種設定基本上就是拋棄了 JIT 可能帶來的性能最佳化優勢. 畢竟直譯器就是一條一條讀進來執行的.
  • -Xcomp: 跟 JVM 說把直譯器關掉, 不要進行直譯式執行, 也有人稱這種設定為最大最佳化級別. 看到這你可能會覺得這是不是非常的高效能? 其實不然, 這個參數會導致 JVM 啟動的速度變慢很多, 同時有些 JIT compiler 的最佳化方式, 如分支預測 (optimistic branch prediction) 這種東西就必須仰賴對 application 進行分析後才會起作用, 所以若開啟了這個參數, 就會讓某些最佳化手段失效了.

從 JDK9 開始, 還引入了另一種新的編譯方式, 叫做 AOT (Ahead-of-Time Compilation), 即直接將 bytecode 編譯成 machine code, 這避免了 JIT 預熱等各方面的開銷.

結論

Java 的執行方式並非只有一種而已, 可以分成直譯式執行與編譯式執行來看:

  • Java source code 透過 javac 指令編譯成 .class 檔案
  • .class 檔案經由 JVM 直譯或編譯執行
    1. 直譯: .class 檔案經過 JVM 內建的直譯器逐行執行
    2. 編譯: JIT 經過分析後把 hot spots 編譯成當前平台的 machine code, 當然這會伴隨著各種程式面的最佳化
    3. JDK9 開始提供的 AOT, 可以把所有 bytecode 編譯成 machie code

References

--

--

Carl

Stand for something or you will fall for anything.