Java 教程

Java 控制語句

面向物件程式設計

Java 內建類

Java 檔案處理

Java 錯誤和異常

Java 多執行緒

Java 同步

Java 網路

Java 集合

Java 介面

Java 資料結構

Java 集合演算法

高階 Java

Java 雜項

Java API 和框架

Java 類參考

Java 有用資源

Java - 即時 (JIT) 編譯器



即時 (JIT) 編譯器是 JVM 內部使用的編譯器,用於將位元組碼中的熱點轉換為機器可理解的程式碼。JIT 編譯器的主要目的是對效能進行大量最佳化。

Java 編譯的程式碼面向 JVM。Java 編譯器 javac 將 Java 程式碼編譯成位元組碼。現在 JVM 解釋此位元組碼並在底層硬體上執行它。如果某些程式碼要反覆執行,JVM 會將該程式碼識別為熱點,並使用 JIT 編譯器 將程式碼進一步編譯到本機機器程式碼級別,並在需要時重用編譯後的程式碼。

讓我們首先了解編譯型語言與解釋型語言之間的區別,以及 Java 如何利用這兩種方法的優勢。

編譯型語言與解釋型語言

諸如 CC++FORTRAN 等語言是編譯型語言。它們的程式碼作為面向底層機器的二進位制程式碼交付。這意味著高階程式碼由專門為底層架構編寫的靜態編譯器一次性編譯成二進位制程式碼。生成的二進位制檔案不會在任何其他架構上執行。

另一方面,解釋型語言(如 PythonPerl)可以在任何機器上執行,只要它們具有有效的直譯器即可。它逐行遍歷高階程式碼,將其轉換為二進位制程式碼。

解釋型程式碼通常比編譯型程式碼慢。例如,考慮一個迴圈。一個直譯器將為迴圈的每次迭代轉換相應的程式碼。另一方面,編譯後的程式碼只會轉換一次。此外,由於直譯器一次只看到一行程式碼,因此它們無法執行任何重要的程式碼最佳化,例如更改語句的執行順序,例如編譯器。

示例

我們將在下面研究此類最佳化的一個示例:

將儲存在記憶體中的兩個數字相加:由於訪問記憶體可能會消耗多個 CPU 週期,因此一個好的編譯器會發出指令從記憶體中獲取資料,並且只有在資料可用時才執行加法運算。它不會等待,並且在此期間執行其他指令。另一方面,在解釋期間不可能進行此類最佳化,因為直譯器在任何給定時間都不瞭解整個程式碼。

但是,解釋型語言可以在任何具有該語言有效直譯器的機器上執行。

Java 是編譯型語言還是解釋型語言?

Java 試圖找到一個折衷方案。由於 JVM 位於 javac 編譯器和底層硬體之間,因此 javac(或任何其他編譯器)編譯器會將 Java 程式碼編譯成位元組碼,位元組碼由特定於平臺的 JVM 理解。然後,JVM 在程式碼執行時使用JIT(即時)編譯將位元組碼編譯成二進位制程式碼。

熱點

在典型的程式中,只有一小部分程式碼會被頻繁執行,並且通常,正是這段程式碼會顯著影響整個應用程式的效能。此類程式碼部分稱為熱點

如果某些程式碼段只執行一次,那麼編譯它將是一種浪費,並且解釋位元組碼會更快。但是,如果該部分是熱點並且執行多次,則 JVM 將對其進行編譯。例如,如果多次呼叫某個方法,那麼編譯程式碼所需的多餘週期將被生成的更快二進位制程式碼抵消。

此外,JVM 執行特定方法或迴圈的次數越多,它收集的資訊就越多,從而進行各種最佳化,以便生成更快的二進位制程式碼。

JIT 編譯器的工作原理

JIT 編譯器透過將某些熱點程式碼編譯成本機程式碼或機器程式碼來幫助提高 Java 程式的執行時間。

JVM 掃描完整程式碼並識別熱點或需要由 JIT 最佳化的程式碼,然後在執行時呼叫 JIT 編譯器,從而提高程式的效率並使其執行得更快。

由於JIT 編譯是一項佔用處理器和記憶體的活動,因此需要相應地規劃 JIT 編譯。

編譯級別

JVM 支援五種編譯級別 -

  • 直譯器
  • 帶有完整最佳化(無分析)的 C1
  • 帶有呼叫和回邊計數器(輕量級分析)的 C1
  • 帶有完整分析的 C1
  • C2(使用來自先前步驟的分析資料)

如果您希望停用所有 JIT 編譯器 並僅使用直譯器,請使用 -Xint

客戶端與伺服器 JIT(即時)編譯器

使用 -client-server 啟用相應的模式。客戶端編譯器(C1)比伺服器編譯器(C2)更早開始編譯程式碼。因此,當 C2 開始編譯時,C1 已經編譯了部分程式碼。
但是,在等待時,C2 會分析程式碼以瞭解比 C1 更多的資訊。因此,等待的時間被可以用來生成更快的二進位制檔案的最佳化所抵消。

從使用者的角度來看,權衡是在程式的啟動時間和程式執行時間之間。如果啟動時間是首要因素,則應使用 C1。如果應用程式預計要執行很長時間(伺服器上部署的應用程式的典型情況),最好使用 C2,因為它會生成更快的程式碼,從而大大抵消任何額外的啟動時間。

對於 IDE(NetBeans、Eclipse)和其他 GUI 程式等程式,啟動時間至關重要。NetBeans 可能需要一分鐘或更長時間才能啟動。當啟動 NetBeans 等程式時,會編譯數百個類。在這種情況下,C1 編譯器是最佳選擇。

請注意,C1 有兩個版本 - 32 位和 64 位。C2 僅提供 64 位版本。

JIT 編譯器最佳化的示例

以下示例展示了 JIT 編譯器最佳化

物件情況下 JIT 最佳化的示例

讓我們考慮以下程式碼 -

for(int i = 0 ; i <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

如果這段程式碼被解釋,直譯器將在每次 for each 迭代中推斷出 obj1 的類。這是因為 Java 中的每個類都有一個 .equals() 方法,該方法擴充套件自 Object 類並且可以被覆蓋。因此,即使 obj1 在每次迭代中都是字串,仍然會進行推斷。

另一方面,實際上會發生的是,JVM 會注意到每次迭代中 obj1 都是 String 類,因此,它會直接生成對應於 String 類的 .equals() 方法 的程式碼。因此,不需要查詢,編譯後的程式碼將執行得更快。

這種行為只有在 JVM 知道程式碼的行為時才有可能。因此,它會在編譯程式碼的某些部分之前等待。

基本資料型別情況下 JIT 最佳化的示例

下面是另一個示例 -

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

對於每個迴圈,直譯器都會從記憶體中獲取 'sum' 的值,將 'i' 加到它上面,並將其儲存回記憶體中。記憶體訪問是一項昂貴的操作,通常需要多個 CPU 週期。由於此程式碼執行多次,因此它是一個熱點。JIT 將編譯此程式碼並進行以下最佳化。

'sum' 的本地副本將儲存在暫存器中,該暫存器特定於某個執行緒。所有操作都將在暫存器中的值上執行,並且當迴圈完成後,該值將被寫回記憶體。

如果其他執行緒也在訪問該變數怎麼辦?由於其他執行緒正在對變數的本地副本進行更新,因此它們將看到過時的值。在這種情況下,需要執行緒同步。一個非常基本的同步原語是將 'sum' 宣告為 volatile。現在,在訪問變數之前,執行緒將重新整理其本地暫存器並從記憶體中獲取該值。訪問它之後,該值會立即寫入記憶體。

即時 (JIT) 編譯器執行的最佳化

以下是 JIT 編譯器執行的一些通用最佳化 -

  • 方法內聯
  • 死程式碼消除
  • 最佳化呼叫站點的啟發式方法
  • 常量摺疊
廣告