點關注,不迷路;持續更新Java架構相關技術及資訊熱文!!!java
JVM 在對代碼執行的優化可分爲運行時(runtime)優化和即時編譯器(JIT)優化。運行時優化主要是解釋執行和動態編譯通用的一些機制,好比說鎖機制(如偏斜鎖)、內存分配機制(如 TLAB)等。除此以外,還有一些專門用於優化解釋執行效率的,好比說模版解釋器、內聯緩存(inline cache,用於優化虛方法調用的動態綁定)。面試
JVM 的即時編譯器優化是指將熱點代碼以方法爲單位轉換成機器碼,直接運行在底層硬件之上。它採用了多種優化方式,包括靜態編譯器可使用的如方法內聯、逃逸分析,也包括基於程序運行 profile 的投機性優化(speculative/optimistic optimization)。這個怎麼理解呢?好比我有一條 instanceof 指令,在編譯以前的執行過程當中,測試對象的類一直是同一個,那麼即時編譯器能夠假設編譯以後的執行過程當中還會是這一個類,而且根據這個類直接返回 instanceof 的結果。若是出現了其餘類,那麼就拋棄這段編譯後的機器碼,而且切換回解釋執行。數組
固然,JVM 的優化方式僅僅做用在運行應用代碼的時候。若是應用代碼自己阻塞了,好比說併發時等待另外一線程的結果,這就不在 JVM 的優化範疇啦。緩存
今天這道面試題在專欄裏有很多同窗問我,也是會在面試時被面試官刨根問底的一個知識點。安全
大多數 Java 工程師並非 JVM 工程師,知識點總歸是要落地的,面試官頗有可能會從實踐的角度探討,例如,如何在生產實踐中,與 JIT 等 JVM 模塊進行交互,落實到如何真正進行實際調優。服務器
在今天這一講,我會從 Java 工程師平常的角度出發,側重於:架構
從總體去了解 Java 代碼編譯、執行的過程,目的是對基本機制和流程有個直觀的認識,以保證可以理解調優選擇背後的邏輯。併發
從生產系統調優的角度,談談將 JIT 的知識落實到實際工做中的可能思路。這裏包括兩部分:如何收集 JIT 相關的信息,以及具體的調優手段。工具
首先,咱們從總體的角度來看看 Java 代碼的整個生命週期,你能夠參考我提供的示意圖測試
我已經提到過,Java 經過引入字節碼這種中間表達方式,屏蔽了不一樣硬件的差別,由 JVM 負責完成從字節碼到機器碼的轉化。
一般所說的編譯期,是指 javac 等編譯器或者相關 API 等將源碼轉換成爲字節碼的過程,這個階段也會進行少許相似常量摺疊之類的優化,只要利用反編譯工具,就能夠直接查看細節。
java優化與 JVM 內部優化也存在關聯,畢竟它負責了字節碼的生成。例如,Java 9 中的字符串拼接,會被 javac 替換成對 StringConcatFactory 的調用,進而爲 JVM 進行字符串拼接優化提供了統一的入口。在實際場景中,還能夠經過不一樣的策略選項來干預這個過程。
今天我要講的重點是JVM 運行時的優化,在一般狀況下,編譯器和解釋器是共同起做用的,具體流程能夠參考下面的示意圖
JVM 會根據統計信息,動態決定什麼方法被編譯,什麼方法解釋執行,即便是已經編譯過的代碼,也可能在不一樣的運行階段再也不是熱點,JVM 有必要將這種代碼從 Code Cache 中移除出去,畢竟其大小是有限的。
Intrinsic 機制,或者叫做內建方法,就是針對特別重要的基礎方法,JDK 團隊直接提供定製的實現,利用匯編或者編譯器的中間表達方式編寫,而後 JVM 會直接在運行時進行替換。
這麼作的理由有不少,例如,不一樣體系結構的 CPU 在指令等層面存在着差別,定製才能充分發揮出硬件的能力。咱們平常使用的典型字符串操做、數組拷貝等基礎方法,Hotspot 都提供了內建實現。
而即時編譯器(JIT),則是更多優化工做的承擔者。JIT 對 Java 編譯的基本單元是整個方法,經過對方法調用的計數統計,甄別出熱點方法,編譯爲本地代碼。另一個優化場景,則是最針對所謂熱點循環代碼,利用一般說的棧上替換技術(OSR,On-Stack Replacement,更加細節請參考R 大的文章),若是方法自己的調用頻度還不夠編譯標準,可是內部有大的循環之類,則仍是會有進一步優化的價值。
從理論上來看,JIT 能夠看做就是基於兩個計數器實現,方法計數器和回邊計數器提供給 JVM 統計數據,以定位到熱點代碼。實際中的 JIT 機制要複雜得多,鄭博士提到了逃逸分析、循環展開、方法內聯等,包括前面提到的 Intrinsic 等通用機制一樣會在 JIT 階段發生。
第二,有哪些手段能夠探查這些優化的具體發生狀況呢?
專欄中已經陸陸續續介紹了一些,我來簡單總結一下並補充部分細節。
打印編譯發生的細節。
輸出更多編譯的細節。
JVM 會生成一個 xml 形式的文件,另外, LogFile 選項是可選的,不指定則會輸出到
具體格式能夠參考 Ben Evans 提供的JitWatch工具和分析指南。
打印內聯的發生,可利用下面的診斷選項,也須要明確解鎖。
如何知曉 Code Cache 的使用狀態呢?
不少工具都已經提供了具體的統計信息,好比,JMC、JConsole 之類,我也介紹過使用 NMT 監控其使用。
第三,咱們做爲應用開發者,有哪些能夠觸手可及的調優角度和手段呢?
調整熱點代碼門限值
我曾經介紹過 JIT 的默認門限,server 模式默認 10000 次,client 是 1500 次。門限大小也存在着調優的可能,可使用下面的參數調整;與此同時,該參數還能夠變相起到下降預熱時間的做用。
不少人可能會產生疑問,既然是熱點,不是遲早會達到門限次數嗎?這個還真未必,由於 JVM 會週期性的對計數的數值進行衰減操做,致使調用計數器永遠不能達到門限值,除了能夠利用 CompileThreshold 適當調整大小,還有一個辦法就是關閉計數器衰減。
若是你是利用 debug 版本的 JDK,還能夠利用下面的參數進行試驗,可是生產版本是不支持這個選項的。
調整 Code Cache 大小
咱們知道 JIT 編譯的代碼是存儲在 Code Cache 中的,須要注意的是 Code Cache 是存在大小限制的,並且不會動態調整。這意味着,若是 Code Cache 過小,可能只有一小部分代碼能夠被 JIT 編譯,其餘的代碼則沒有選擇,只能解釋執行。因此,一個潛在的調優勢就是調整其大小限制。
固然,也能夠調整其初始大小。
注意,在相對較新版本的 Java 中,因爲分層編譯(Tiered-Compilation)的存在,Code Cache 的空間需求大大增長,其自己默認大小也被提升了。
調整編譯器線程數,或者選擇適當的編譯器模式
JVM 的編譯器線程數目與咱們選擇的模式有關,選擇 client 模式默認只有一個編譯線程,而 server 模式則默認是兩個,若是是當前最廣泛的分層編譯模式,則會根據 CPU 內核數目計算 C1 和 C2 的數值,你能夠經過下面的參數指定的編譯線程數。
在強勁的多處理器環境中,增大編譯線程數,可能更加充分的利用 CPU 資源,讓預熱等過程更加快速;可是,反之也可能致使編譯線程爭搶過多資源,尤爲是當系統很是繁忙時。例如,系統部署了多個 Java 應用實例的時候,那麼減少編譯線程數目,則是能夠考慮的。
生產實踐中,也有人推薦在服務器上關閉分層編譯,直接使用 server 編譯器,雖然會致使稍慢的預熱速度,可是可能在特定工做負載上會有微小的吞吐量提升。
其餘一些相對邊界比較混淆的所謂「優化」
好比,減小進入安全點。嚴格說,它遠遠不僅是發生在動態編譯的時候,GC 階段發生的更加頻繁,你能夠利用下面選項診斷安全點的影響。
注意,在 JDK 9 以後,PrintGCApplicationStoppedTime 已經被移除了,你須要使用「-Xlog:safepoint」之類方式來指定。
不少優化階段均可能和安全點相關,例如:
在 JIT 過程當中,逆優化等場景會須要插入安全點。
常規的鎖優化階段也可能發生,好比,偏斜鎖的設計目的是爲了不無競爭時的同步開銷,可是當真的發生競爭時,撤銷偏斜鎖會觸發安全點,是很重的操做。因此,在併發場景中偏斜鎖的價值實際上是被質疑的,常常會明確建議關閉偏斜鎖。
點關注,不迷路;持續更新Java架構相關技術及資訊熱文!!!