otSpot,家喻戶曉的JVM,我們的Java和Scala程序就運行在它上面。年復一年,一次又一次的迭代,經過無數工程師的不斷優化,現在它的代碼執行的速度和效率已經逼近本地編譯的代碼了。
它的核心是一個JIT(Just-In-Time)編譯器。JIT只有一個目的,就是為了提升你代碼的執行速度,這也是HotSpot能如此流行和成功的重要因素。
JIT編譯器都做了什么?
你的代碼在執行的時候,JVM會收集它運行的相關數據。一旦收集到了足夠的數據,證明某個方法是熱點(默認是1萬次調用),JIT就會介入進來,將“運行緩慢的”平臺獨立的的字節碼轉化成本地編譯的,優化瘦身后的版本。
有些優化是顯而易見的:比如簡單方法內聯,刪除無用代碼,將庫函數調用替換成本地方法等。不過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)被去掉了,盡管也帶來了一些代碼重復(兩處append),不過性能上得到了提升。
邊界條件優化
檢查空指針是很常見的一個操作。有時候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方法,這個判斷永遠不會失敗。
在多次執行這段代碼并且一直沒有進入到if語句之后,JIT編譯器會認為這個檢查很多可能是多余的。然后它會重新編譯這個方法,把這個檢查去掉,最后代碼看起來就像是這樣的:
public static String l33tify(String phrase) {
return phrase.replace('e', '3');
}
這能顯著的提升性能,而且在很多時候這么優化是沒有問題的。
那萬一這個樂觀的假設實際上是錯了呢?
JVM現在執行的已經是本地代碼了,空引用可不會引起NullPointerException,而是真正的嚴重的內存訪問沖突,JVM是個低級生物,它會去處理這個段錯誤,然后恢復執行沒有優化過的代碼——這個編譯器可再也不敢認為它是多余的了:它會重新編譯代碼,這下空指針的檢查又回來了。
虛方法內聯
JVM的JIT編譯器和其它靜態編譯器的最大不同就是,JIT編譯器有運行時的動態數據,它可以基于這些數據進行決策。
方法內聯是編譯器一個常見的優化,編譯器將方法調用替換成實際調用的代碼,以避免一次調用的開銷。不過當碰到虛方法調用(動態分發)的話情況就需要點小技巧了。
先看下這段代碼 :
public class Main {
public static void perform(Song s) {
s.sing();
}
}
public interface Song { void sing(); }
public class GangnamStyle implements Song {
@Override
public void sing() {
System.out.println("Oppan gangnam style!");
}
}
public class Baby implements Song {
@Override
public void sing() {
System.out.println("And I was like baby, baby, baby, oh");
}
}
perform方法可能會被調用了無數次,每次都會調用sing方法。方法調用的開銷當然是很大的,尤其像這種,因為它需要根據運行時s的類型來動態選擇具體執行的代碼。在這里,方法內聯看真來像是遙不可及的夢想,對吧?
原文轉自:http://it.deepinmind.com/jvm/2014/03/28/jvm-performance-magic-tricks.html