http://www.deepinmind.com/jvm/2014/06/28/a-little-bit-on-jvm-and-jit.html
你或許也知道,正是JVM( Java Virtusal Machine,Java虛擬機)使得Java成為遵循“一次編寫,處處運行”的范例。JVM包括如下核心組件:
堆是你的應(yīng)用程序代碼中new操作符分配內(nèi)存的地方。棧存儲的是你在某個方法作用域內(nèi)要進(jìn)行賦值的那些本地變量。有一點需要注意的是,方法作用域內(nèi)所定義的變量在方法結(jié)束后將會被刪除。比如說,一個String變量在方法內(nèi)被賦值了,它的作用域是本地作用域,那么它將會被存儲到棧里,而給它所賦的值則是存儲在堆中。
持久代空間是用來存儲類及方法的數(shù)據(jù)以及應(yīng)用程序中定義的靜態(tài)變量。方法區(qū)其實就是持久代空間中的一塊區(qū)域,它將會存儲所有的方法,字段,常量池的詳細(xì)數(shù)據(jù)。
JIT編譯器和代碼緩存密不可分。JVM核心會在運行時將Java字節(jié)碼解釋成匯編代碼。這個解釋的過程是非常緩慢的,因為每次執(zhí)行你的應(yīng)用程序的代碼時都需要將字節(jié)碼轉(zhuǎn)化成機器代碼。這就是JIT編譯器發(fā)揮作用的地方了,它會將方法編譯好然后存儲到代碼緩存中。
JIT編譯器會在運行時分析應(yīng)用程序的代碼,來識別出哪些方法可以歸類為熱方法。在這里熱方法意味著代碼段會被頻繁地訪問。JIT編譯器給每個方法都分配一個計數(shù)器,以便統(tǒng)計它們的使用頻率。當(dāng)計數(shù)器達(dá)到預(yù)定義的閾值時,這個方法會被JIT編譯器編譯成對應(yīng)的匯編代碼,然后存儲到代碼緩存中。現(xiàn)在,當(dāng)JIT需要再調(diào)用這些已經(jīng)被編譯好并存儲到代碼緩存中的方法時,它不用再去解釋執(zhí)行了,而是可以使用代碼緩存中已編譯好的匯編代碼。這能提升你的應(yīng)用程序的執(zhí)行效率,因為使用編譯好的代碼要比運行時去解釋要快得多。
當(dāng)提及JIT編譯器時,由于缺少相關(guān)的文檔,有兩個主要的因素我們大多數(shù)人可能都不太了解。它們分別是:
默認(rèn)使用哪個編譯器取決于對應(yīng)程序運行的機器的體系結(jié)構(gòu)以及JVM的版本(32位還是64位的)。我們來看下它們分別有什么作用。
客戶端編譯器在應(yīng)用啟動的時候就會將你的字節(jié)碼編譯成匯編代碼。這間接意味著會增加你的應(yīng)用程序的啟動時間。不過它最大的缺點在于你的代碼緩存可能很快就會用光你的內(nèi)存。很多優(yōu)化只有當(dāng)你的程序運行了一段時間才能夠進(jìn)行。不過由于客戶端編譯器已經(jīng)占用了代碼緩存的空間,你可能沒有地方去存儲這些優(yōu)化后的匯編代碼了。這就是服務(wù)端編譯器要勝出的地方。
服務(wù)端編譯器不像客戶端編譯器那樣,它不會在應(yīng)用啟動的時候就編譯代碼。它會讓應(yīng)用程序的代碼運行一段時間(這也被稱為預(yù)熱階段),然后它才會開始將字節(jié)碼編譯成匯編代碼,最終將它們存儲到代碼緩存里。
我的下一篇文章將會討論如何可以將客戶端及服務(wù)端編譯給結(jié)合起來,同時還將介紹幾個很少用到的JVM參數(shù),但它們對提升應(yīng)用的性能至關(guān)重要。
==============Java HotSpot VM中的JIT編譯
http://ifeve.com/hotspot-jit/
本文是Java HotSpot VM and just-in-time(JIT) compilation系列的第一篇。
Java HotSpot虛擬機是Oracle收購Sun時獲得的,JVM和開源的OpenJDK都是以此虛擬機為基礎(chǔ)發(fā)展的。如同其它虛擬機,HotSpot虛擬機為字節(jié)碼提供了一個運行時環(huán)境。實際上,它主要會做這三件事情:
- 執(zhí)行方法所請求的指令和運算。
- 定位、加載和驗證新的類型(即類加載)。
- 管理應(yīng)用內(nèi)存。
最后兩點都是各自領(lǐng)域的大話題,所以這篇文章中只關(guān)注代碼執(zhí)行。
JIT編譯
Java HotSpot是一個混合模式的虛擬機,也就是說它既可以解釋字節(jié)碼,又可以將代碼編譯為本地機器碼以更快的執(zhí)行。通過配置-XX:+PrintCompilation參數(shù),你可以在log文件中看到方法被JIT編譯時的信息。JIT編譯發(fā)生在運行時 —— 方法經(jīng)過多次運行之后。到方法需要使用到的時候,HotSpot VM會決定如何優(yōu)化這些代碼。
如果你好奇JIT編譯帶來的性能提升,可以使用-Djava.compiler=none將其關(guān)掉然后運行基準(zhǔn)測試程序來看看它們的差別。
Java HotSpot虛擬機可以運行在兩種模式下:client或者server。你可以在JVM啟動時通過配置-client或者-server選項來選擇其中一種。兩種模式都有各自的適用場景,本文中,我們只會涉及到server模式。
兩種模式最主要的區(qū)別是server模式下會進(jìn)行更激進(jìn)的優(yōu)化 —— 這些優(yōu)化是建立在一些并不永遠(yuǎn)為真的假設(shè)之上。一個簡單的保護(hù)條件(guard condition)會驗證這些假設(shè)是否成立,以確保優(yōu)化總是正確的。如果假設(shè)不成立,Java HotSpot虛擬機將會撤銷所做的優(yōu)化并退回到解釋模式。也就是說Java HotSpot虛擬機總是會先檢查優(yōu)化是否仍然有效,不會因為假設(shè)不再成立而表現(xiàn)出錯誤的行為。
在server模式下,Java HotSpot虛擬機會默認(rèn)在解釋模式下運行方法10000次才會觸發(fā)JIT編譯。可以通過虛擬機參數(shù)-XX:CompileThreshold來調(diào)整這個值。比如-XX:CompileThreshold=5000會讓觸發(fā)JIT編譯的方法運行次數(shù)減少一半。(譯者注:有關(guān)JIT觸發(fā)條件可參考《深入理解Java虛擬機》第十一章以及《Java Performance》第三章HotSpot VM JIT Compilers小節(jié))
這可能會誘使新手將編譯閾值調(diào)整到一個非常低的值。但要抵擋住這個誘惑,因為這樣可能會降低虛擬機性能,優(yōu)化后減少的方法執(zhí)行時間還不足以抵消花在JIT編譯上的時間。
當(dāng)Java HotSpot虛擬機能為JIT編譯收集到足夠多的統(tǒng)計信息時,性能會最好。當(dāng)你降低編譯閾值時,Java HotSpot虛擬機可能會在非熱點代碼的編譯中花費較多時間。有些優(yōu)化只有在收集到足夠多的統(tǒng)計信息時才會進(jìn)行,所以降低編譯閾值可能導(dǎo)致優(yōu)化效果不佳。
另外一方面,很多開發(fā)者想讓一些重要方法在編譯模式下盡快獲得更好的性能。
解決此問題一般是在進(jìn)程啟動后,對代碼進(jìn)行預(yù)熱以使它們被強制編譯。對于像訂單系統(tǒng)或者交易系統(tǒng)來說,重要的是要確保預(yù)熱不會產(chǎn)生真實的訂單。
Java HotSpot虛擬機提供了很多參數(shù)來輸出JIT的編譯信息。最常用的就是前文提到的PrintCompilation,也還有一些其它參數(shù)。
接下來我們將使用PrintCompilation來觀察Java HotSpot虛擬機在運行時編譯方法的成效。但先有必要說一下用于計時的System.nanoTime()方法。
計時方法
Java為我們提供了兩個主要的獲取時間值的方法:currentTimeMillis()和nanoTime().前者對應(yīng)于我們在實體世界中看到的時間(所謂的鐘表時間),它的精度能滿足大多數(shù)情況,但不適用于低延遲的應(yīng)用。
納秒計時器擁有更高的精度。這種計時器度量時間的間隔極短。1納秒是光在光纖中移動20CM所需的時間,相比之下,光通過光纖從倫敦傳送到紐約大約需要27.5毫秒。
因為納秒級的時間戳精度太高,使用不當(dāng)就會產(chǎn)生較大誤差,因此使用時需要注意。
如,currentTimeMillis()能很好的在機器間同步,可以用于測量網(wǎng)絡(luò)延遲,但nanoTime()不能跨機器使用。
接下來將上面的理論付諸實踐,來看一個很簡單(但極其強大)的JIT編譯技術(shù)。
方法內(nèi)聯(lián)
方法內(nèi)聯(lián)是編譯器優(yōu)化的關(guān)鍵手段之一。方法內(nèi)聯(lián)就是把方法的代碼“復(fù)制”到發(fā)起調(diào)用的方法里,以消除方法調(diào)用。這個功能相當(dāng)重要,因為調(diào)用一個小方法可能比執(zhí)行該小方法的方法體耗時還多。
JIT編譯器可以進(jìn)行漸進(jìn)內(nèi)聯(lián),開始時內(nèi)聯(lián)簡單的方法,如果可以進(jìn)行其它優(yōu)化時,就接著優(yōu)化內(nèi)聯(lián)后的較大的代碼塊。
Listing1,Listing1A以及Listing1B是個簡單的測試,將直接操作字段和通過getter/setter方法做了對比。如果簡單的getters和setters方法沒有使用內(nèi)聯(lián)的話,那調(diào)用它們的代價是相當(dāng)大的,因為方法調(diào)用比直接操作字段代價更高。
Listing1:
查看源代碼 打印幫助 | 02 | ????private static double timeTestRun(String desc, int runs, |
| 03 | ????????Callable<Double> callable) throws Exception { |
| 04 | ????????long start = System.nanoTime(); |
| 05 | ????????callable.call(); |
| 06 | ????????long time = System.nanoTime() - start; |
| 07 | ????????return (double) time / runs; |
| 10 | ????// Housekeeping method to provide nice uptime values for us |
| 11 | ????private static long uptime() { |
| 12 | ????????return ManagementFactory.getRuntimeMXBean().getUptime() + 15; |
| 16 | ????public static void main(String... args) throws Exception { |
| 17 | ????????int iterations = 0; |
| 18 | ????????for (int i : new int[] |
| 19 | ????????????{ 100, 1000, 5000, 9000, 10000, 11000, 13000, 20000, 100000} ) { |
| 20 | ????????????final int runs = i - iterations; |
| 21 | ????????????iterations += runs; |
| 23 | ????????????// NOTE: We return double (sum of values) from our test cases to |
| 24 | ????????????// prevent aggressive JIT compilation from eliminating the loop in |
| 25 | ????????????// unrealistic ways |
| 26 | ????????????Callable<Double> directCall = new DFACaller(runs); |
| 27 | ????????????Callable<Double> viaGetSet = new GetSetCaller(runs); |
| 29 | ????????????double time1 = timeTestRun("public fields", runs, directCall); |
| 30 | ????????????double time2 = timeTestRun("getter/setter fields", runs, viaGetSet); |
| 32 | ????????????System.out.printf("%7d %,7d\t\tfield access=%.1f ns, getter/setter=%.1f ns%n", |
| 33 | ????????????????uptime(), iterations, time1, time2); |
| 34 | ????????????// added to improve readability of the output |
| 35 | ????????????Thread.sleep(100); |
Listing1A:
查看源代碼 打印幫助 | 01 | public class DFACaller implements Callable<Double>{ |
| 02 | ????private final int runs; |
| 04 | ????public DFACaller(int runs_) { |
| 09 | ????public Double call() { |
| 10 | ????????DirectFieldAccess direct = new DirectFieldAccess(); |
| 11 | ????????double sum = 0; |
| 12 | ????????for (int i = 0; i < runs; i++) { |
| 13 | ????????????direct.one++; |
| 14 | ????????????sum += direct.one; |
| 20 | public class DirectFieldAccess { |
Listing1B:
查看源代碼 打印幫助 | 01 | public class GetSetCaller implements Callable<Double> { |
| 02 | ????private final int runs; |
| 04 | ????public GetSetCaller(int runs_) { |
| 09 | ????public Double call() { |
| 10 | ????????ViaGetSet getSet = new ViaGetSet(); |
| 11 | ????????double sum = 0; |
| 12 | ????????for (int i = 0; i < runs; i++) { |
| 13 | ????????????getSet.setOne(getSet.getOne() + 1); |
| 14 | ????????????sum += getSet.getOne(); |
| 20 | public class ViaGetSet { |
| 23 | ????public int getOne() { |
| 27 | ????public void setOne(int one) { |
| 28 | ????????this.one = one; |
如果使用java -cp. -XX:PrintCompilation Main 運行測試用例,就能看到性能上的差異(見Listing2)。
Listing2
31 1 java.lang.String::hashCode (67 bytes) 36 100 field access=1970.0 ns, getter/setter=1790.0 ns 39 2 sun.nio.cs.UTF_8$Encoder::encode (361 bytes) 42 3 java.lang.String::indexOf (87 bytes)
141 1,000 field access=16.7 ns, getter/setter=67.8 ns
245 5,000 field access=16.8 ns, getter/setter=72.8 ns
245 4 ViaGetSet::getOne (5 bytes)
348 9,000 field access=16.0 ns, getter/setter=65.3 ns
450 5 ViaGetSet::setOne (6 bytes)
450 10,000 field access=16.0 ns, getter/setter=199.0 ns
553 6 Main$1::call (51 bytes)
554 7 Main$2::call (51 bytes)
556 8 java.lang.String::charAt (33 bytes)
556 11,000 field access=1263.0 ns, getter/setter=1253.0 ns
658 13,000 field access=5.5 ns, getter/setter=1.5 ns
760 20,000 field access=0.7 ns, getter/setter=0.7 ns
862 100,000 field access=0.7 ns, getter/setter=0.7 ns
這些是什么意思?Listing2中的第一列是程序啟動到語句執(zhí)行時所經(jīng)過的毫秒數(shù),第二列是方法ID(編譯后的方法)或遍歷次數(shù)。
注意:測試中沒有直接使用String和UTF_8類,但它們?nèi)匀怀霈F(xiàn)在編譯的輸出中,這是因為平臺使用了它們。
從Listing2中的第二行可以發(fā)現(xiàn),直接訪問字段和通過getter/setter都是比較慢的,這是因為第一次運行時包含了類加載的時間,下一行就比較快了,盡管此時還沒有任何代碼被編譯。
另外要注意下面幾點:
- 在遍歷1000和5000次時,直接操作字段比使用getter/setter方法快,因為getter 和setter還沒有內(nèi)聯(lián)或優(yōu)化。即便如此,它們都還相當(dāng)?shù)乜臁?
- 在遍歷9000次時,getter方法被優(yōu)化了(因為每次循環(huán)中調(diào)用了兩次),使性能有小許提高。
- 在遍歷10000次時,setter方法也被優(yōu)化了,因為需要額外花費時間去優(yōu)化,所以執(zhí)行速度降下來了。
- 最終,兩個測試類都被優(yōu)化了:
- DFACaller直接操作字段,GetSetCaller使用getter和setter方法。此時它們不僅剛被優(yōu)化,還被內(nèi)聯(lián)了。
- 從下一次的遍歷中可以看到,測試用例的執(zhí)行時間仍不是最快的。
- 在13000次遍歷之后,兩種字段訪問方式的性能都和最后更長時間測試的結(jié)果一樣好,我們已經(jīng)達(dá)到了性能的穩(wěn)定狀態(tài)。
需要特別注意的是,直接訪問字段和通過getter/setter訪問在穩(wěn)定狀態(tài)下的性能是基本一致的,因為方法已經(jīng)被內(nèi)聯(lián)到GetSetCaller中,也就是說在viaGetSet中所做的事情和directCall中完全一樣。
JIT編譯是在后臺進(jìn)行的。每次可用的優(yōu)化手段可能隨機器的不同而不同,甚至,同個程序的多次運行期間也可能不一樣。
總結(jié)
這篇文章中,我所描述的只是JIT編譯的冰山一角,尤其是沒有提到如何寫出好的基準(zhǔn)測試以及如何使用統(tǒng)計信息以確保不會被平臺的動態(tài)性所愚弄。
這里使用的基準(zhǔn)測試非常簡單,不適合做為真實的基準(zhǔn)測試。在第二部分,我計劃向您展示一個真實的基準(zhǔn)測試并繼續(xù)深入JIT編譯的過程。
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖
總結(jié)
以上是生活随笔為你收集整理的小谈JVM及JIT的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。