深入理解JVM之編譯最佳化

541732025發表於2015-03-29
JDK在原始碼編譯階段將原始碼編譯為JVM位元組碼,JVM位元組碼是一種平臺無關的中間程式碼方式,要由JVM在執行期間對其進行解釋並執行,這種方式成為位元組碼解釋執行方式。
對於物件導向的語言而言,最重要的是執行方法的指令,JVM有一套自己的執行方法的指令:invokestatic(呼叫static方法)、invokevirtual(呼叫物件例項的方法)、invokeinterface(呼叫介面的方法)、invokespecial(呼叫private方法和編譯原始碼後生成的方法,此方法為物件例項化時的初始化方法)

位元組碼是在棧中執行
執行緒建立時,會產生程式計數器(PC)、棧,PC存放下一條要執行的指令在方法內的偏移量。棧中存放棧幀,棧幀主要分為區域性變數區、運算元棧兩部分。
區域性變數區用於存放方法的區域性變數和引數,運算元棧用於存放方法執行過程中產生的中間結果,棧幀中還有一些其它空間,如方法已解析的常量池引用。

深入理解JVM之編譯最佳化

點選(此處)摺疊或開啟

  1. void foo(){
  2.     int a = 1;
  3.     int b = 2;
  4.     int c = (a + b) * 5;
  5. }
編譯後位元組碼:
code:
0:iconst_1 //將型別為int、值為1的常量放入運算元棧
1: istore_0 //將運算元棧中棧頂的值彈出放入區域性變數區
2:iconst_2 //將型別為int、值為2的常量放入運算元棧
3:istore_1 //將運算元棧中棧頂的值彈出放入區域性變數區
4:iload_0 //裝載區域性變數區中第一個值到運算元棧
5:iload_1 //裝載區域性變數區中第二個值到運算元棧
6:iadd //執行int型別的add指令,並將計算結果放入運算元棧
7:iconst_5 //將型別為int、值為5的常量放入運算元棧
8:imul //執行int型別的mul指令,並將計算結果放入運算元棧
9:istore_2 //將運算元棧中棧頂的值彈出並放入區域性變數區
10:return //返回

編譯執行
解釋執行的效率較低,為提升程式碼的執行效能,JVM提供將位元組碼編譯為機器碼的支援,編譯在執行時進行,通常稱為JIT編譯器,JVM在執行過程中對執行頻率高的程式碼進行編譯,對執行不頻繁的程式碼則繼續採用解釋執行的方式。
編譯執行有兩種模式:client compiler(-client)和server compiler(-server)
client compiler又稱為C1,較輕量級,只做少量效能開銷比較高的最佳化,它佔用記憶體少,適合桌面互動式應用,它的最佳化方式主要有:方法內聯,去虛擬化,冗餘削除等。
1,方法內聯:在方法中需要呼叫其它方法,需要經歷引數傳遞、返回值傳遞及跳轉等,方法內聯即把呼叫到的方法的指令直接植入到當前方法中
2,去虛擬化:在裝載class之後,進行類層次的分析,如發現介面的方法只提供一個實現類,那麼對於呼叫了此方法的程式碼,也可以進行方法內聯。
3,冗餘削除:在編譯時,根據執行時狀況進行程式碼摺疊或削除。去掉不需要的程式碼指令。
Server compiler又稱為C2,較為重量級,C2採用大量傳統編譯最佳化技巧,佔用記憶體多,適用於伺服器端應用。
“逃逸分析”是C2進行很多最佳化的基礎,逃逸分析是指根據執行狀況來判斷方法中的變數是否會被外部讀取,如不會則認為此變數是逃逸的,基於逃逸分析C2在編譯時會做標量替換、棧上分配、同步削除等
1,標量替換:用標量替換聚合量,見程式碼:
Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);
當point物件在後面的執行過程中未用到時,經過編譯後,程式碼會變成類似下面的結構:
int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);
這種方式的好處是,如果建立的物件並未用到其中的全部變數,則可節省一定的記憶體,對於程式碼執行而言,由於無需去找物件的引用,也會更快一些。
2,棧上分配:如果上例中,point是逃逸的,那麼C2會選擇在棧上直接建立point物件例項,而不是在JVM堆上,在棧上分配的好處一方面是快速,另方面是垃圾回收時隨著方法的結束,物件也就被回收了。
3,同步削除:指同步的物件逃逸,方法外部沒有引用到同步的物件,那就沒有同步的必要了,C2編譯時會直接去掉同步。

JVM會根據機器配置來選擇C1還是C2,當機器配置CPU達到2核且記憶體超過2G則選擇C2,但是32位windows機器上始終選擇C1模式,也可在啟動時透過-client或-server來強制指定。
基於這個特性,在對java程式碼進行效能測試時,要注意是否實現做了足夠次數的呼叫,以保證測試是公平的。對於高效能的程式而言,也應考慮在程式提供給使用者訪問前,自行進行一定的呼叫,以保證關鍵功能的效能。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28912557/viewspace-1478062/,如需轉載,請註明出處,否則將追究法律責任。

相關文章