在本章中,我們將學習JIT編譯器,以及編譯語言和解釋語言之間的區別。
編譯語言與解釋語言
C,C++和FORTRAN等語言是編譯語言。代碼以二進位代碼的形式提供,目標是底層機器。高級代碼由專門為底層架構編寫的靜態編譯器一次編譯成二進位代碼。生成的二進位檔不能在任何其他體系結構上運行。
另一方面,Python和Perl等解釋語言可以在任何機器上運行,只要它們具有有效的解釋器即可。它在高級代碼上逐行進行,將其轉換為二進位代碼。
解釋代碼通常比編譯代碼慢。例如,考慮一個迴圈。解釋將轉換迴圈的每次迭代的相應代碼。另一方面,編譯代碼翻譯只生成一個二進位檔。解釋器一次只能看到一行,因此無法執行任何重要的代碼,例如,更改編譯器等語句的執行順序。
我們將在下面研究優化的例子 -
添加存儲在內存中的兩個數字。由於訪問記憶體可能會消耗多個CPU週期,因此良好的編譯器將發出指令以從記憶體中獲取數據,並僅在數據可用時執行添加。它不會等待,同時執行其他指令。另一方面,在解釋期間不可能進行這樣的優化,因為解釋器在任何給定時間都不知道整個代碼。
但是,解釋語言可以在任何具有該語言的有效解釋器的機器上運行。
Java是編譯還是解釋語言?
Java試圖找到一個中間立場。由於JVM位於javac編譯器和底層硬體之間,因此javac(或任何其他編譯器)編譯器在Bytecode中編譯Java代碼,這是由特定於平臺的JVM所理解的。然後,當代碼執行時,JVM使用JIT(即時)編譯以二進位編譯位元組碼。
熱點(HotSpots)
在典型的程式中,只有一小部分代碼經常執行,而且通常,這段代碼會顯著影響整個應用程式的性能。這些代碼段稱為HotSpots。
如果某些代碼段只執行一次,那麼編譯它將是一種浪費,而且解釋位元組碼會更快。但是如果該部分是一個熱點(HotSpots)部分並且執行多次,那麼JVM將編譯它。例如,如果一個方法被多次調用,那麼編譯代碼所需的額外週期將生成的更快的二進位檔所抵消。
此外,JVM運行特定方法或迴圈越多,它收集的資訊越多,以進行各種優化,從而生成更快的二進位檔。
考慮以下代碼 -
for(int i = 0 ; I <= 100; i++) {
System.out.println(obj1.equals(obj2)); //two objects
}
這個代碼中,解釋器會為每次迭代推導出obj1
類。這是因為Java中的每個類都有一個equals()
方法,它從Object
類擴展並可以覆蓋。
另一方面,實際發生的是JVM會注意到,對於每次迭代,obj1
都是String
類,因此,它會直接生成與String
類的equals()
方法對應的代碼。因此不需要查找,並且編譯的代碼將執行得更快。
只有當JVM知道代碼的行為時,才能實現這種行為。因此,它在編譯代碼的某些部分之前等待。
以下是另一個例子 -
int sum = 7;
for(int i = 0 ; i <= 100; i++) {
sum += i;
}
對於每個迴圈,解釋器從記憶體中獲取sum
的值,向其添加I
,並將其存儲回記憶體。記憶體訪問是一項昂貴的操作,通常需要多個CPU週期。由於此代碼多次運行,因此它是HotSpot。JIT將編譯此代碼並進行以下優化。
sum
的本地副本將存儲在特定於特定線程的寄存器中。所有操作都將對寄存器中的值進行,當迴圈完成時,該值將寫回記憶體。
如果其他線程也在訪問變數怎麼辦?由於某些其他線程正在對變數的本地副本進行更新,因此它們會看到過時的值。在這種情況下需要線程同步。一個非常基本的同步原語是將sum
聲明為volatile
。在訪問變數之前,線程將刷新其本地寄存器並從記憶體中獲取值。訪問後,該值立即寫入記憶體。
以下是JIT編譯器完成的一些常規優化 -
- 方法內聯
- 死代碼消除
- 用於優化調用站點的啟發式演算法
- 不斷折疊