JVM性能魔术技巧
HotSpot是我們眾所周知和喜愛的JVM,是Java和Scala汁流淌的大腦。 多年來,許多工程師對其進行了改進和調整,并且在每次迭代中,其代碼執行的速度和效率都接近本機編譯代碼。
JIT(“即時”)編譯器是其核心。 該組件的唯一目的是使您的代碼快速運行,這是HotSpot如此受歡迎和成功的原因之一。
JIT編譯器實際上是做什么的?
在執行代碼時,JVM會收集有關其行為的信息。 一旦收集了有關熱方法的足夠統計信息(默認閾值為10K調用),編譯器就會啟動,并將該方法的與平臺無關的“慢”字節碼轉換為自身的優化,精簡,平均編譯版本。
一些優化是顯而易見的:簡單的方法內聯,清除無效代碼,用本機數學運算替換庫調用等。請注意,JIT編譯器不會就此停止。 這是它執行的一些更有趣的優化:
分而治之
您使用以下模式多少次:
StringBuilder sb = new StringBuilder("Ingredients: ");for (int i = 0; i < ingredients.length; i++) {if (i > 0) {sb.append(", ");}sb.append(ingredients[i]); }return sb.toString();也許這個:
boolean nemoFound = false;for (int i = 0; i < fish.length; i++) {String curFish = fish[i];if (!nemoFound) {if (curFish.equals("Nemo")) {System.out.println("Nemo! There you are!");nemoFound = true;continue;}}if (nemoFound) {System.out.println("We already found Nemo!");} else {System.out.println("We still haven't found Nemo : (");} }這兩個循環的共同點是,在這兩種情況下,循環都會做一件事一段時間,然后從某個角度開始做另一件事。 編譯器可以發現這些模式,并將循環分成多個案例,或“剝離”幾次迭代。
讓我們以第一個循環為例。 if (i > 0)行在一次迭代中從false開始,并且從那一點開始始終計算為true 。 為何每次都要檢查狀況? 編譯器將編譯該代碼,就像這樣編寫:
StringBuilder sb = new StringBuilder("Ingredients: ");if (ingredients.length > 0) {sb.append(ingredients[0]);for (int i = 1; i < ingredients.length; i++) {sb.append(", ");sb.append(ingredients[i]);} }return sb.toString();這樣,即使某些代碼可能在進程中重復,冗余的if (i > 0)也將被刪除,因為速度就是它的全部。
生活在邊緣
空檢查是一丁點的。 有時null對于我們的引用是有效值(例如,指示缺少值或錯誤),但有時為了安全起見,我們添加了null檢查。
其中一些檢查可能永遠不會失敗(就此而言,null表示失敗)。 一個經典的示例將包含一個斷言,如下所示:
public static String l33tify(String phrase) {if (phrase == null) {throw new IllegalArgumentException("phrase must not be null");}return phrase.replace('e', '3'); }如果您的代碼運行良好,并且從未將null作為l33tify的參數l33tify ,則斷言將永遠不會失敗。
在多次執行此代碼而沒有進入if語句的主體之后,JIT編譯器可能會樂觀地認為此檢查很有可能是不必要的。 然后它將繼續編譯該方法,將檢查全部丟棄,就好像是這樣寫的:
public static String l33tify(String phrase) {return phrase.replace('e', '3'); }這可以顯著提高性能,這在大多數情況下可能是純粹的勝利。
但是,如果那個幸福道路的假設最終被證明是錯誤的呢?
由于JVM現在正在執行本機已編譯的代碼,因此null引用不會導致模糊的NullPointerException ,而是導致實際的,苛刻的內存訪問沖突。 JVM是它的低級生物,它將攔截產生的分段錯誤,進行恢復,并進行反優化處理-編譯器不能再假設null檢查是多余的:它重新編譯該方法,這次使用null檢查。
虛擬精神錯亂
JVM的JIT編譯器與其他靜態編譯器(如C ++編譯器)之間的主要區別之一是,JIT編譯器具有動態運行時數據,決策時可以依靠該數據來運行。
方法內聯是一種常見的優化方法,在該方法中,編譯器采用一個完整的方法并將其代碼插入另一個程序中,以避免調用方法。 在處理虛擬方法調用(或動態調度 )時,這會有些棘手。
以以下代碼為例:
public class Main {public static void perform(Song s) {s.sing();} }public interface Song { void sing(); }public class GangnamStyle implements Song {@Overridepublic void sing() {System.out.println("Oppan gangnam style!");} }public class Baby implements Song {@Overridepublic void sing() {System.out.println("And I was like baby, baby, baby, oh");} }// More implementations here該方法perform可能被執行數百萬次,每一次方法的調用sing發生。 調用是昂貴的,尤其是諸如此類的調用,因為調用需要根據s的運行時類型每次動態選擇要執行的實際代碼。 在這一點上,內聯似乎是一個遙不可及的夢想,不是嗎?
不必要! 執行后, perform幾千次,編譯器可能會決定,根據其收集的統計數據,該調用的95%的目標的一個實例GangnamStyle 。 在這些情況下,HotSpot JIT可以執行樂觀優化,以消除虛擬的sing調用。 換句話說,編譯器將為這些代碼生成本機代碼:
public static void perform(Song s) {if (s fastnativeinstanceof GangnamStyle) {System.out.println("Oppan gangnam style!");} else {s.sing();} }由于此優化依賴于運行時信息,因此即使它是多態的,它也可以消除大多數sing調用。
JIT編譯器還有很多技巧,但是這些只是一些技巧,可讓您了解當我們的代碼由JVM執行和優化時的幕后故事。
我是否能幫助?
JIT編譯器是面向簡單人員的編譯器; 它旨在優化簡單的編寫,并搜索出現在日常標準代碼中的模式。 幫助您的編譯器的最好方法是不要太努力地幫助它-只需編寫代碼即可。
翻譯自: https://www.javacodegeeks.com/2013/06/jvm-performance-magic-tricks.html
總結
- 上一篇: 重启wifi后怎么设置(重启wifi后怎
- 下一篇: 顺风车路线设置技巧(顺风车设置顺路程度)