JVM的几点性能优化
HotSpot,家喻戶曉的JVM,我們的Java和Scala程序就運(yùn)行在它上面。年復(fù)一年,一次又一次的迭代,經(jīng)過無數(shù)工程師的不斷優(yōu)化,現(xiàn)在它的代碼執(zhí)行的速度和效率已經(jīng)逼近本地編譯的代碼了。
它的核心是一個(gè)JIT(Just-In-Time)編譯器。JIT只有一個(gè)目的,就是為了提升你代碼的執(zhí)行速度,這也是HotSpot能如此流行和成功的重要因素。
JIT編譯器都做了什么?
你的代碼在執(zhí)行的時(shí)候,JVM會(huì)收集它運(yùn)行的相關(guān)數(shù)據(jù)。一旦收集到了足夠的數(shù)據(jù),證明某個(gè)方法是熱點(diǎn)(默認(rèn)是1萬次調(diào)用),JIT就會(huì)介入進(jìn)來,將“運(yùn)行緩慢的”平臺(tái)獨(dú)立的的字節(jié)碼轉(zhuǎn)化成本地編譯的,優(yōu)化瘦身后的版本。
有些優(yōu)化是顯而易見的:比如簡(jiǎn)單方法內(nèi)聯(lián),刪除無用代碼,將庫函數(shù)調(diào)用替換成本地方法等。不過JIT編譯的威力遠(yuǎn)不止此。下面列舉了它的一些非常有意思的優(yōu)化:
分而治之
你是不是經(jīng)常會(huì)這樣寫代碼:
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 : (");} }這兩個(gè)例子的共同之處是,循環(huán)體里先是處理這個(gè)事情,過一段時(shí)間又處理另外一件。編譯器可以識(shí)別出這些情況,它可以將循環(huán)拆分成不同的分支,或者將幾次迭代單獨(dú)剝離。
我們來說下第一個(gè)例子。if(i>0)第一次的時(shí)候是false,后面就一直是true。為什么要每次都判斷這個(gè)呢?編譯器會(huì)對(duì)它進(jìn)行優(yōu)化,就好像你是這樣寫的一樣:
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)被去掉了,盡管也帶來了一些代碼重復(fù)(兩處append),不過性能上得到了提升。
邊界條件優(yōu)化
檢查空指針是很常見的一個(gè)操作。有時(shí)候null是一個(gè)有效的值(比如,表明缺少某個(gè)值,或者出現(xiàn)錯(cuò)誤),有時(shí)候檢查空指針是為了代碼能正常運(yùn)行。
有些檢查是永遠(yuǎn)不會(huì)失敗的(在這里null代表失敗)。這里有一個(gè)典型的場(chǎng)景:
public static String l33tify(String phrase) { if (phrase == null) { throw new IllegalArgumentException("phrase must not be null"); } return phrase.replace('e', '3'); }如果你代碼寫得好的話,沒有傳null值給l33tify方法,這個(gè)判斷永遠(yuǎn)不會(huì)失敗。
在多次執(zhí)行這段代碼并且一直沒有進(jìn)入到if語句之后,JIT編譯器會(huì)認(rèn)為這個(gè)檢查很多可能是多余的。然后它會(huì)重新編譯這個(gè)方法,把這個(gè)檢查去掉,最后代碼看起來就像是這樣的:
public static String l33tify(String phrase) { return phrase.replace('e', '3'); }這能顯著的提升性能,而且在很多時(shí)候這么優(yōu)化是沒有問題的。
那萬一這個(gè)樂觀的假設(shè)實(shí)際上是錯(cuò)了呢?
JVM現(xiàn)在執(zhí)行的已經(jīng)是本地代碼了,空引用可不會(huì)引起NullPointerException,而是真正的嚴(yán)重的內(nèi)存訪問沖突,JVM是個(gè)低級(jí)生物,它會(huì)去處理這個(gè)段錯(cuò)誤,然后恢復(fù)執(zhí)行沒有優(yōu)化過的代碼——這個(gè)編譯器可再也不敢認(rèn)為它是多余的了:它會(huì)重新編譯代碼,這下空指針的檢查又回來了。
虛方法內(nèi)聯(lián)
JVM的JIT編譯器和其它靜態(tài)編譯器的最大不同就是,JIT編譯器有運(yùn)行時(shí)的動(dòng)態(tài)數(shù)據(jù),它可以基于這些數(shù)據(jù)進(jìn)行決策。
方法內(nèi)聯(lián)是編譯器一個(gè)常見的優(yōu)化,編譯器將方法調(diào)用替換成實(shí)際調(diào)用的代碼,以避免一次調(diào)用的開銷。不過當(dāng)碰到虛方法調(diào)用(動(dòng)態(tài)分發(fā))的話情況就需要點(diǎn)小技巧了。
先看下這段代碼 :
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方法可能會(huì)被調(diào)用了無數(shù)次,每次都會(huì)調(diào)用sing方法。方法調(diào)用的開銷當(dāng)然是很大的,尤其像這種,因?yàn)樗枰鶕?jù)運(yùn)行時(shí)s的類型來動(dòng)態(tài)選擇具體執(zhí)行的代碼。在這里,方法內(nèi)聯(lián)看真來像是遙不可及的夢(mèng)想,對(duì)吧?
當(dāng)然不是了。在多次執(zhí)行perform方法后,編譯器會(huì)根據(jù)它收集的數(shù)據(jù)發(fā)現(xiàn),95%的調(diào)用對(duì)象都是GangnamStyle實(shí)例。這樣的話,JIT編譯器會(huì)很樂觀將虛方法的調(diào)用優(yōu)化掉。也就是說,編譯器會(huì)直接生成本地代碼 ,對(duì)應(yīng)的Java實(shí)現(xiàn)大概是這樣的:
public static void perform(Song s) { if (s fastnativeinstanceof GangnamStyle) { System.out.println("Oppan gangnam style!"); } else { s.sing(); } }由于這個(gè)優(yōu)化取決于運(yùn)行時(shí)信息,它可以優(yōu)化掉大部分的sing方法調(diào)用,盡管這個(gè)方法是多態(tài)的。
JIT編譯器還有很多很有意思的技巧,這只是介紹了其中的幾點(diǎn),讓你能感覺到我們代碼在執(zhí)行的時(shí)候,JVM在底層都做了些什么優(yōu)化。
我能幫助JIT做些什么優(yōu)化嗎
JIT編譯器是針對(duì)一般人的編譯器;它是用來優(yōu)化正常寫出的代碼的,它會(huì)去分析日常標(biāo)準(zhǔn)寫法中的一些模式。不要刻意寫代碼去幫助JIT編譯器進(jìn)行優(yōu)化就是對(duì)它最好的幫助 ——就正常寫你自己的代碼就好了。
譯注:JIT還有許多很多意思的優(yōu)化,這里只是列舉出了幾點(diǎn)。當(dāng)然了,你也不用太在意它,就像文中最后說的,正常寫好自己的代碼就好了。
HotSpot,家喻戶曉的JVM,我們的Java和Scala程序就運(yùn)行在它上面。年復(fù)一年,一次又一次的迭代,經(jīng)過無數(shù)工程師的不斷優(yōu)化,現(xiàn)在它的代碼執(zhí)行的速度和效率已經(jīng)逼近本地編譯的代碼了。
它的核心是一個(gè)JIT(Just-In-Time)編譯器。JIT只有一個(gè)目的,就是為了提升你代碼的執(zhí)行速度,這也是HotSpot能如此流行和成功的重要因素。
JIT編譯器都做了什么?
你的代碼在執(zhí)行的時(shí)候,JVM會(huì)收集它運(yùn)行的相關(guān)數(shù)據(jù)。一旦收集到了足夠的數(shù)據(jù),證明某個(gè)方法是熱點(diǎn)(默認(rèn)是1萬次調(diào)用),JIT就會(huì)介入進(jìn)來,將“運(yùn)行緩慢的”平臺(tái)獨(dú)立的的字節(jié)碼轉(zhuǎn)化成本地編譯的,優(yōu)化瘦身后的版本。
有些優(yōu)化是顯而易見的:比如簡(jiǎn)單方法內(nèi)聯(lián),刪除無用代碼,將庫函數(shù)調(diào)用替換成本地方法等。不過JIT編譯的威力遠(yuǎn)不止此。下面列舉了它的一些非常有意思的優(yōu)化:
分而治之
你是不是經(jīng)常會(huì)這樣寫代碼:
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 : (");} }這兩個(gè)例子的共同之處是,循環(huán)體里先是處理這個(gè)事情,過一段時(shí)間又處理另外一件。編譯器可以識(shí)別出這些情況,它可以將循環(huán)拆分成不同的分支,或者將幾次迭代單獨(dú)剝離。
我們來說下第一個(gè)例子。if(i>0)第一次的時(shí)候是false,后面就一直是true。為什么要每次都判斷這個(gè)呢?編譯器會(huì)對(duì)它進(jìn)行優(yōu)化,就好像你是這樣寫的一樣:
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)被去掉了,盡管也帶來了一些代碼重復(fù)(兩處append),不過性能上得到了提升。
邊界條件優(yōu)化
檢查空指針是很常見的一個(gè)操作。有時(shí)候null是一個(gè)有效的值(比如,表明缺少某個(gè)值,或者出現(xiàn)錯(cuò)誤),有時(shí)候檢查空指針是為了代碼能正常運(yùn)行。
有些檢查是永遠(yuǎn)不會(huì)失敗的(在這里null代表失敗)。這里有一個(gè)典型的場(chǎng)景:
public static String l33tify(String phrase) { if (phrase == null) { throw new IllegalArgumentException("phrase must not be null"); } return phrase.replace('e', '3'); }如果你代碼寫得好的話,沒有傳null值給l33tify方法,這個(gè)判斷永遠(yuǎn)不會(huì)失敗。
在多次執(zhí)行這段代碼并且一直沒有進(jìn)入到if語句之后,JIT編譯器會(huì)認(rèn)為這個(gè)檢查很多可能是多余的。然后它會(huì)重新編譯這個(gè)方法,把這個(gè)檢查去掉,最后代碼看起來就像是這樣的:
public static String l33tify(String phrase) { return phrase.replace('e', '3'); }這能顯著的提升性能,而且在很多時(shí)候這么優(yōu)化是沒有問題的。
那萬一這個(gè)樂觀的假設(shè)實(shí)際上是錯(cuò)了呢?
JVM現(xiàn)在執(zhí)行的已經(jīng)是本地代碼了,空引用可不會(huì)引起NullPointerException,而是真正的嚴(yán)重的內(nèi)存訪問沖突,JVM是個(gè)低級(jí)生物,它會(huì)去處理這個(gè)段錯(cuò)誤,然后恢復(fù)執(zhí)行沒有優(yōu)化過的代碼——這個(gè)編譯器可再也不敢認(rèn)為它是多余的了:它會(huì)重新編譯代碼,這下空指針的檢查又回來了。
虛方法內(nèi)聯(lián)
JVM的JIT編譯器和其它靜態(tài)編譯器的最大不同就是,JIT編譯器有運(yùn)行時(shí)的動(dòng)態(tài)數(shù)據(jù),它可以基于這些數(shù)據(jù)進(jìn)行決策。
方法內(nèi)聯(lián)是編譯器一個(gè)常見的優(yōu)化,編譯器將方法調(diào)用替換成實(shí)際調(diào)用的代碼,以避免一次調(diào)用的開銷。不過當(dāng)碰到虛方法調(diào)用(動(dòng)態(tài)分發(fā))的話情況就需要點(diǎn)小技巧了。
先看下這段代碼 :
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方法可能會(huì)被調(diào)用了無數(shù)次,每次都會(huì)調(diào)用sing方法。方法調(diào)用的開銷當(dāng)然是很大的,尤其像這種,因?yàn)樗枰鶕?jù)運(yùn)行時(shí)s的類型來動(dòng)態(tài)選擇具體執(zhí)行的代碼。在這里,方法內(nèi)聯(lián)看真來像是遙不可及的夢(mèng)想,對(duì)吧?
當(dāng)然不是了。在多次執(zhí)行perform方法后,編譯器會(huì)根據(jù)它收集的數(shù)據(jù)發(fā)現(xiàn),95%的調(diào)用對(duì)象都是GangnamStyle實(shí)例。這樣的話,JIT編譯器會(huì)很樂觀將虛方法的調(diào)用優(yōu)化掉。也就是說,編譯器會(huì)直接生成本地代碼 ,對(duì)應(yīng)的Java實(shí)現(xiàn)大概是這樣的:
public static void perform(Song s) { if (s fastnativeinstanceof GangnamStyle) { System.out.println("Oppan gangnam style!"); } else { s.sing(); } }由于這個(gè)優(yōu)化取決于運(yùn)行時(shí)信息,它可以優(yōu)化掉大部分的sing方法調(diào)用,盡管這個(gè)方法是多態(tài)的。
JIT編譯器還有很多很有意思的技巧,這只是介紹了其中的幾點(diǎn),讓你能感覺到我們代碼在執(zhí)行的時(shí)候,JVM在底層都做了些什么優(yōu)化。
我能幫助JIT做些什么優(yōu)化嗎
JIT編譯器是針對(duì)一般人的編譯器;它是用來優(yōu)化正常寫出的代碼的,它會(huì)去分析日常標(biāo)準(zhǔn)寫法中的一些模式。不要刻意寫代碼去幫助JIT編譯器進(jìn)行優(yōu)化就是對(duì)它最好的幫助 ——就正常寫你自己的代碼就好了。
譯注:JIT還有許多很多意思的優(yōu)化,這里只是列舉出了幾點(diǎn)。當(dāng)然了,你也不用太在意它,就像文中最后說的,正常寫好自己的代碼就好了。
原創(chuàng)文章轉(zhuǎn)載請(qǐng)注明出處:JVM的幾點(diǎn)性能優(yōu)化
英文原文鏈接
總結(jié)
以上是生活随笔為你收集整理的JVM的几点性能优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎样腌制咸鸭蛋(腌制咸鸭蛋的三种方法)
- 下一篇: 火把节举行哪三个仪式