JVM優化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架構相關技術及資訊熱文!!!

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