JVM性能優化,第2部分:編譯器JVM

通過優銳課的java學習分享中,整理了部分關於JVM的相關知識點,分享給大家參考學習,如有不足之處,歡迎 補充!

Java編譯器在JVM性能優化系列的第二篇文章中佔據中心位置。 Eva Andreasson介紹了不同種類的編譯器,並比較了客戶端,服務器和分層編譯的性能結果。最後,她概述了常見的JVM優化,例如消除死代碼,內聯和循環優化。

Java編譯器是Java著名的平臺的獨立性的來源。軟件開發人員會盡力編寫最好的Java應用程序,然後編譯器會在幕後進行工作,以爲目標目標平臺生成高效且性能良好的執行代碼。不同種類的編譯器可滿足各種應用程序需求,從而產生特定的所需性能結果。你對編譯器瞭解得越多,就它們的工作方式和可用的種類而言,你就越能夠優化Java應用程序性能。

JVM性能優化系列的第二篇文章重點介紹了各種Java虛擬機編譯器之間的區別。我還將討論Java即時(JIT)編譯器使用的一些常見優化。 (有關JVM概述和該系列的介紹,請參見“ JVM性能優化,第1部分”。)

什麼是編譯器?

簡而言之,編譯器將編程語言作爲輸入,併產生可執行語言作爲輸出。 一種常見的編譯器是Javac,它包含在所有標準Java開發工具包(JDK)中。 javac將Java代碼作爲輸入並將其轉換爲字節碼-JVM的可執行語言。 字節碼存儲在.class文件中,當啓動Java進程時,該文件將加載到Java運行時中。

字節碼不能被標準CPU讀取,需要轉換爲底層執行平臺可以理解的指令語言。 JVM中負責將字節碼轉換爲可執行平臺指令的組件是另一個編譯器。 一些JVM編譯器可以處理多個級別的轉換。 例如,編譯器可能在將字節碼轉換成實際的機器指令(即翻譯的最後一步)之前,創建字節碼的各種中間表示形式。

從平臺不可知的角度來看,我們希望儘可能使代碼獨立於平臺,以便最後的翻譯級別(從最低表示到實際的機器代碼)是將執行鎖定到特定平臺的處理器體系結構的步驟 。 靜態和動態編譯器之間的最高級別隔離。 從那裏開始,我們有選擇,這取決於我們要針對的執行環境,所需的性能結果以及需要滿足的資源限制。 我在本系列的第1部分中簡要討論了靜態和動態編譯器。 在以下各節中,我將進一步解釋。

靜態與動態編譯

靜態編譯器的一個示例是前面提到的javac。 對於靜態編譯器,輸入代碼將被解釋一次,而輸出可執行文件的形式將在執行程序時使用。 除非你對原始源代碼進行更改並重新編譯代碼(使用編譯器),否則輸出將始終產生相同的結果。 這是因爲輸入是靜態輸入,而編譯器是靜態編譯器。

在靜態編譯中,


static int add7( int x ) {
      return x+7;}

會導致類似於以下字節碼的內容:

iload0
 bipush 7
 iadd
 ireturn

動態編譯器會動態地將一種語言翻譯成另一種語言,這意味着它會在執行代碼時發生-在運行時! 動態編譯和優化爲運行時提供了能夠適應應用程序負載變化的優勢。 動態編譯器非常適合Java運行時,這些運行時通常在不可預測且不斷變化的環境中執行。 大多數JVM使用動態編譯器,例如即時(JIT)編譯器。 問題是動態編譯器和代碼優化有時需要額外的數據結構,線程和CPU資源。 優化或字節碼上下文分析越高級,編譯消耗的資源就越多。 與輸出代碼的顯着性能相比,在大多數環境中,開銷仍然很小。

JVM種類和Java平臺的獨立性

所有JVM實現都有一個共同點,那就是它們試圖將應用程序字節碼轉換爲機器指令。 一些JVM在加載時解釋應用程序代碼,並使用性能計數器來關注“熱”代碼。 一些JVM跳過解釋,僅依靠編譯。 編譯的資源密集性可能會受到更大的影響(尤其是對於客戶端應用程序),但它還可以實現更高級的優化。

從Java字節碼到執行

將Java代碼編譯爲字節碼後,下一步就是將字節碼指令轉換爲機器碼。 這可以由解釋器或編譯器完成。

解釋

字節碼編譯的最簡單形式稱爲解釋。 解釋器只需爲每個字節碼指令查找硬件指令,然後將其發送出去以由CPU執行。

你可能會想到解釋,類似於使用字典:對於特定單詞(字節碼指令),存在確切的翻譯(機器碼指令)。 由於解釋器一次讀取並立即執行一個字節碼指令,因此沒有機會對指令集進行優化。 每次調用字節碼時,解釋器也必須執行解釋,這使它相當慢。 解釋是執行代碼的一種準確方法,但是未優化的輸出指令集可能不是目標平臺處理器的最高性能序列。

總結

另一方面,編譯器會將要執行的整個代碼加載到運行時中。在翻譯字節碼時,它可以查看整個或部分運行時上下文,並就如何實際翻譯代碼做出決策。它的決策基於對代碼圖的分析,例如對指令和運行時上下文數據的不同執行分支。

當將字節碼序列轉換爲機器碼指令集並可以對該指令集進行優化時,替換指令集(例如優化序列)將存儲到稱爲代碼緩存的結構中。下次執行該字節碼時,先前優化的代碼可以立即位於代碼緩存中,並用於執行。在某些情況下,性能計數器可能會加入並覆蓋之前的優化,在這種情況下,編譯器將運行新的優化序列。代碼緩存的優點是可以立即執行生成的指令集-無需解釋性查找或編譯!這加快了執行時間,尤其是對於Java方法,其中多次調用相同的方法。

優化

除了動態編譯外,還可以插入性能計數器。 例如,編譯器可能會插入一個性能計數器,以在每次調用字節碼塊(例如對應於特定方法)時進行計數。 編譯器使用有關給定字節碼有多“熱”的數據來確定代碼中的最優化將對運行中的應用程序產生最佳影響。 運行時概要分析數據使編譯器可以即時制定豐富的代碼優化決策集,從而進一步提高代碼執行性能。 隨着更完善的代碼概要分析數據的可用,它可以用於做出其他更好的優化決策,例如:如何更好地以編譯爲語言對指令進行排序,是否用更高效的指令集代替指令集,甚至 是否消除冗餘操作。


考慮一下Java代碼:

static int add7( int x ) {
      return x+7;}

這可以由javac靜態編譯爲字節碼:

iload0
 bipush 7
 iadd
 ireturn

調用該方法時,字節碼塊將動態編譯爲機器指令。 當性能計數器(如果存在於代碼塊中)達到閾值時,它可能也會得到優化。 對於給定的執行平臺,最終結果可能類似於以下機器指令集:

lea rax,[rdx+7]
ret

不同應用程序的不同編譯器

不同的Java應用程序有不同的需求。 長期運行的企業服務器端應用程序可以進行更多優化,而較小的客戶端應用程序可能需要以最小的資源消耗來快速執行。 讓我們考慮三種不同的編譯器設置及其各自的優缺點。

客戶端編譯器
著名的優化編譯器是C1,它是通過-client JVM啓動選項啓用的編譯器。 顧名思義,C1是客戶端編譯器。 它是爲客戶端應用程序設計的,這些客戶端應用程序具有較少的可用資源,並且在許多情況下對應用程序啓動時間敏感。 C1使用性能計數器進行代碼性能分析,以實現簡單,相對無干擾的優化。

服務器端編譯器
對於長時間運行的應用程序(例如服務器端企業Java應用程序),客戶端編譯器可能不夠。 可以使用類似C2的服務器端編譯器。 通常通過在啓動命令行中添加JVM啓動選項-server來啓用C2。 由於大多數服務器端程序預計將運行很長時間,因此啓用C2意味着你將比使用運行時間短的輕量級客戶端應用程序收集更多的性能分析數據。 因此,你將能夠應用更高級的優化技術和算法。

服務器編譯器比客戶端編譯器處理更多的概要分析數據,並且允許進行更復雜的分支分析,這意味着它將考慮哪種優化路徑會更有利。具有更多可用的概要分析數據可產生更好的應用程序結果。當然,進行更廣泛的分析和分析需要在編譯器上花費更多的資源。啓用了C2的JVM將使用更多的線程和更多的CPU週期,需要更大的代碼緩存,依此類推。

分層編譯
分層編譯將客戶端和服務器端編譯組合在一起。 Azul首先在其Zing JVM中提供了分層編譯。最近(從Java SE 7開始),Oracle Java Hotspot JVM已採用它。分層編譯利用了JVM中客戶端和服務器編譯器的優勢。客戶端編譯器在應用程序啓動期間最活躍,並處理由較低的性能計數器閾值觸發的優化。客戶端編譯器還會插入性能計數器併爲更高級的優化準備指令集,服務器端編譯器將在稍後階段解決這些問題。分層編譯是一種非常節省資源的性能分析方法,因爲編譯器能夠在影響較小的編譯器活動期間收集數據,以後可以將其用於更高級的優化。與僅使用解釋的代碼配置文件計數器所獲得的信息相比,這種方法還可以產生更多的信息。

圖1中的圖表架構描述了純解釋,客戶端,服務器端和分層編譯之間的性能差異。 X軸顯示執行時間(時間單位),Y軸性能(操作數/時間單位)。

與純解釋代碼相比,使用客戶端編譯器可以使執行性能(以ops / s爲單位)提高約5至10倍,從而提高了應用程序性能。增益的變化當然取決於編譯器的效率,啓用或實現的優化方式以及(在較小程度上)應用程序相對於目標執行平臺的良好設計。不過,後者確實是Java開發人員永遠不必擔心的事情。

與客戶端編譯器相比,服務器端編譯器通常可將代碼性能提高30%到50%。在大多數情況下,性能改進將平衡額外的資源成本。

分層編譯結合了兩種編譯器的最佳功能。客戶端編譯可縮短啓動時間並加快優化速度,而服務器端編譯可在執行週期後期提供更高級的優化。
一些常見的編譯器優化

消除無效代碼
消除無效代碼聽起來像是:消除從未被調用的代碼-即 無效 代碼。 如果編譯器在運行時發現某些指令是不必要的,它將僅從執行指令集中消除它們。 例如,在清單1中,從不使用變量的特定值分配,並且可以在執行時將其完全忽略。 在字節碼級別,這可能對應於永遠不需要執行將值加載到寄存器中的操作。 不必執行加載意味着更少的CPU時間,從而縮短了代碼執行速度,進而縮短了應用程序的時間-尤其是當代碼很熱並且每秒被調用幾次時。

清單1顯示了Java代碼,該代碼示例了一個從未使用過的變量,這是不必要的操作。
清單 1. 清除無效代碼

int timeToScaleMyApp(boolean endlessOfResources) {
   int reArchitect = 24;
   int patchByClustering = 15;
   int useZing = 2;

   if(endlessOfResources)
       return reArchitect + useZing;
   else
       return useZing;}

在字節碼級別上,如果加載了一個值但從未使用過,則編譯器可以檢測到該值並消除死代碼,如清單2所示。從不執行加載可以節省CPU時間,從而提高程序的執行速度。

清單2.優化後的相同代碼


int timeToScaleMyApp(boolean endlessOfResources) {
   int reArchitect = 24;
   //unnecessary operation removed here...
   int useZing = 2;

   if(endlessOfResources)
       return reArchitect + useZing;
   else
       return useZing;}

冗餘消除是類似的優化,它刪除重複的指令以提高應用程序性能。

內聯
許多優化嘗試消除機器級別的跳轉指令(例如,用於x86架構的JMP)。 跳轉指令更改指令指針寄存器,從而傳輸執行流程。 相對於其他ASSEMBLY指令,這是一項昂貴的操作,這就是爲什麼它是減少或消除的常見目標的原因。 針對此的非常有用且衆所周知的優化稱爲內聯。 由於跳轉很昂貴,因此將許多頻繁調用具有不同入口地址的小型方法的調用內插會很有幫助。 清單3至5中的Java代碼體現了內聯的好處。

清單3.調用者方法

int whenToEvaluateZing(int y) {
   return daysLeft(y) + daysLeft(0) + daysLeft(y+1);}

清單4.調用的方法

int daysLeft(int x){
   if (x == 0)
      return 0;
   else
      return x - 1;}

清單5.內聯方法

int whenToEvaluateZing(int y){
   int temp = 0;

   if(y == 0) temp += 0; else temp += y - 1;
   if(0 == 0) temp += 0; else temp += 0 - 1;
   if(y+1 == 0) temp += 0; else temp += (y + 1) - 1;

   return temp; }

在清單3至清單5中,調用方法對一個小方法進行了3次調用,出於示例的考慮,我們認爲對內聯方法而言,跳轉到三遍更爲有益。

內聯很少調用的方法可能不會有太大的區別,但是內聯經常調用的所謂“熱”方法可能意味着性能上的巨大差異。 內聯還經常爲進一步的優化讓路,如清單6所示
清單6.內聯之後,可以應用更多的優化


int whenToEvaluateZing(int y){
   if(y == 0) return y;
   else if (y == -1) return y - 1;
   else return y + y - 1;}

循環優化
循環優化在減少執行循環帶來的開銷方面發揮着重要作用。 在這種情況下,開銷意味着昂貴的跳轉,條件檢查的次數,非最佳指令流水線(即導致CPU不工作或額外循環的指令順序)。 循環優化有很多種,總計有很多優化。 值得注意的包括:

合併循環:當兩個附近的循環重複相同的時間時,如果主體中沒有相互引用的情況,則編譯器可以嘗試合併循環的主體,以同時(並行)執行,即它們彼此完全獨立。
反轉循環:基本上,你將常規的while循環替換爲do-while循環。並且do-while循環設置爲if子句。這種替換導致更少的兩次跳躍。但是,它增加了條件檢查,因此增加了代碼大小。此優化是一個很好的示例,說明了如何使用更多的資源來提高代碼效率-編譯器必須在運行時動態評估和決定成本與收益的平衡。
切片循環:重新組織循環,以便對大小適合緩存的數據塊進行迭代。
展開循環:減少必須評估循環條件的次數以及跳轉次數。你可以將其視爲“內聯”要執行的主體的多次迭代,而不會越過循環條件。展開循環會帶來風險,因爲展開循環可能會損害流水線並導致多次冗餘指令提取,從而降低性能。再次,這是編譯器在運行時做出的判斷調用,即,如果增益足夠,則成本可能是值得的。

概述了編譯器在字節碼級別(及以下)上爲提高應用程序在目標平臺上的執行性能所做的工作。 討論的優化是常見且流行的,但僅簡要介紹了可用的選項。

結論:反思要點和重點
針對不同需求使用不同的編譯器。

解釋是將字節碼轉換爲機器指令的最簡單形式,並且基於指令查找表進行工作。
編譯器允許基於性能計數器進行優化,但需要一些額外的資源(代碼緩存,優化線程等)
與解釋型代碼相比,客戶端編譯器將執行代碼的性能提高了一個數量級(提高了5到10倍)。
服務器端編譯器比客戶端編譯器將應用程序性能提高了30%到50%,但佔用了更多資源。
分層編譯提供了兩個方面的優勢。 啓用客戶端編譯可以使你的代碼快速獲得良好的性能,並隨時間進行服務器編譯,以使經常調用的代碼執行得更好。
有許多可能的代碼優化。編譯器的一項重要任務是分析所有可能性,並權衡使用優化的成本與輸出機器碼的執行速度優勢。

Eva Andreasson從事Java虛擬機技術,SOA,雲計算和其他企業中間件解決方案的研究已有10年了。她於2001年以JRockit JVM的開發人員身份加入了初創公司Appeal Virtual Solutions(後來被BEA Systems收購)。 Eva已獲得垃圾回收啓發式方法和算法的兩項專利。她還開創了確定性垃圾收集的先河,後來通過JRockit Real Time進行了產品化。 Eva與Sun和Intel在技術合作夥伴關係以及JRockit產品組,WebLogic和Coherence的各種集成項目(在2008年被甲骨文收購後)方面密切合作。 2009年,Eva加入了Azul Systems,擔任新Zing Java平臺的產品經理。最近,她調職並加入Cloudera團隊,擔任Cloudera Hadoop發行版的高級產品經理,在那裏她致力於高度可擴展的分佈式數據處理框架的激動人心的未來和創新之路。

喜歡這篇文章的可以點個贊,歡迎大家留言評論,記得關注我,每天持續更新技術乾貨、職場趣事、海量面試資料等等
 > 如果你對java技術很感興趣也可以交流學習,共同學習進步。 
不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代

文章寫道這裏,歡迎完善交流。最後奉上近期整理出來的一套完整的java架構思維導圖,分享給大家對照知識點參考學習。有更多JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java乾貨
JVM性能優化,第2部分:編譯器JVM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章