關于Java性能的9個謬論
Java的性能有某種黑魔法之稱。部分原因在于Java平臺非常復雜,很多情況下問題難以定位。然而在歷史上還有一種趨勢,人們靠智慧和經(jīng)驗來研究Java性能,而不是靠應用統(tǒng)計和實證推理。在這篇文章中,我希望拆穿一些最荒謬的技術神話。
1.Java很慢關于Java的性能有很多謬論,這一條是最過時的,可能也是最為明顯的。
確實,在上世紀90年代和本世紀初處,Java有時是很慢。
然而從那以后,虛擬機和JIT技術已經(jīng)有了十多年的改進,Java的整體性能現(xiàn)在已經(jīng)非常好了。
在6個獨立的Web性能基準測試中,Java框架在24項測試中有22項位列前四。
盡管JVM利用性能剖析僅優(yōu)化常用的代碼路徑,但這種優(yōu)化效果很明顯。很多情況下,JIT編譯的Java代碼和C++一樣快,而且這樣的情況越來越多了。
盡管如此,依然有人認為Java平臺很慢,這或許源自體驗過Java平臺早期版本的人的歷史偏見。
在下結(jié)論之前,我們建議保持客觀的態(tài)度,并且評估一下最新的性能結(jié)果。
2.可以孤立地看待單行Java代碼考慮下面這行短小的代碼:
MyObject obj = new MyObject();
對Java開發(fā)者而言,看似很明顯,這行代碼一定會分配一個對象并調(diào)用適當?shù)臉嬙炱鳌?/p>
我們也許可以據(jù)此推出性能邊界了。我們認為這行代碼一定會導致執(zhí)行一定量的工作,基于這種推定,就可以嘗試計算其性能影響了。
其實這種認識是錯誤的,它讓我們先入為主地認為,不管什么工作,在任何情況下都會進行。
事實上,javac和JIT編譯器都能夠?qū)⑺来a優(yōu)化掉。就JIT編譯器而言,基于性能剖析數(shù)據(jù),甚至可以通過預測將代碼優(yōu)化掉。在這樣的情況下,這行代碼根本不會運行,所以不會影響性能。
此外,在某些JVM中——比如JRockit——JIT編譯器甚至可以將對象上的操作分解,這樣即便代碼路徑還有效,分配操作也可以避免。
這里的寓意是,在處理Java性能問題時,上下文非常重要,過早的優(yōu)化有可能產(chǎn)生違反直覺的結(jié)果。所以最好不好過早優(yōu)化。相反,應該總是構建代碼,并且使用性能調(diào)校技術來定位性能熱點,然后加以改進。
3.微基準測試和你想象的一樣正如我們上面看到的那樣,檢查一小段代碼不如分析應用的整體性能來的準確。
盡管如此,開發(fā)者還是喜歡編寫微基準測試。似乎對平臺底層的某些方面進行修修補補會帶來無窮的樂趣。
理查德·費曼曾經(jīng)說過:“不要欺騙自己,你自己正是最容易被欺騙的人。”這句話用來說明編寫Java微基準測試這件事是再合適不過了。
編寫良好的微基準測試極其困難。Java平臺非常復雜,而且很多微基準測試只能用于測量瞬時效應,或是Java平臺的其他意想不到的方面。
例如,如果沒有經(jīng)驗,編寫的微基準測試往往就是測一下時間或垃圾收集,卻沒有抓住真正的影響因素。
只有那些有實際需求的開發(fā)者和開發(fā)團隊才應該編寫微基準測試。這些基準測試應該完全公開(包括源代碼),而且是可以復現(xiàn)的,還應接受同行評審及進一步的審查。
Java平臺的很多優(yōu)化表明統(tǒng)計運行和單次運行對結(jié)果影響很大。要得到真實可靠的答案,應該將一個單獨的基準測試運行多次,然后把結(jié)果匯總到一起。
如果讀者感覺有必要編寫微基準測試,Georges、Buytaert和Eeckhout等人的論文《利用嚴格的統(tǒng)計方法評測Java 性能(Statistically Rigorous Java Performance Evaluation)》是個不錯的開始。缺乏適當?shù)慕y(tǒng)計分析,我們很容易被誤導。
有很多開發(fā)好的工具以及圍繞這些工具的社區(qū)(比如Google的Caliper)。如果確實有必要編寫微基準測試,那也不要自己編寫,這時需要的是同行的意見和經(jīng)驗。
4.算法慢是性能問題的最常見原因在開發(fā)者之間有一個很常見的認知錯誤(普通大眾也是如此),即認為系統(tǒng)中他們控制的那部分很重要。
在探討Java性能時,這種認知錯誤也有所體現(xiàn):Java開發(fā)者認為算法的質(zhì)量是性能問題的主要原因。開發(fā)者考慮的是代碼,因此他們自然會偏向于考慮自己的算法。
實際上在處理一系列現(xiàn)實中的性能問題時,人們發(fā)現(xiàn)算法設計是根本問題的幾率不足10%。
相反,與算法相比,垃圾收集、數(shù)據(jù)庫訪問和配置錯誤導致應用程序緩慢的可能性更大。
大部分應用處理的數(shù)據(jù)量相對較小,因此,即使主要算法效率不高,通常也不會導致嚴重的性能問題。可以肯定,我們的算法不是最優(yōu)的;盡管如此,算法帶來的性能問題還是算小的,更多性能問題是應用棧的其他部分導致的。
因此我們的最佳建議是,使用實際生產(chǎn)數(shù)據(jù)來揭開性能問題的真正原因。要測量性能數(shù)據(jù),而不是憑空猜測!
5.緩存可以解決所有問題“計算機科學中的所有問題都可以通過引入一個中間層來解決。”
David Wheeler的這句程序員格言(在互聯(lián)網(wǎng)上,這句話至少還被認為是其他兩位計算機科學家說的)非常常見,尤其是在Web開發(fā)者之中很流行。
如果未能透徹理解現(xiàn)有的架構,而且分析也已停頓,往往就是“緩存可以解決所有問題”這種謬論抬頭的時候了。
在開發(fā)者看來,與其處理嚇人的現(xiàn)有系統(tǒng),還不如在前面加一層緩存,將現(xiàn)有系統(tǒng)隱藏起來,以此期待最好的情況。無疑,這種方式只是讓整體架構更復雜了,當下一個接手的開發(fā)者打算了解系統(tǒng)現(xiàn)狀時,情況會更糟糕。
規(guī)模龐大、設計拙劣的系統(tǒng)往往缺乏整體的設計,是一次一行代碼、一個子系統(tǒng)這樣寫出來的。然而很多情況下,簡化并重構架構會帶來更好的性能,而且?guī)缀蹩偸歉菀鬃屓死斫狻?/p>
所以當評估是否真的有必要加入緩存時,應該先計劃收集一些基本的使用統(tǒng)計信息(比如命中率和未命中率等),以此證明緩存層帶來的真正價值。
6.所有應用都需要關注Stop-The-World問題Java平臺存在一個無法改變的事實:為運行垃圾收集,所有應用線程必須周期性停頓。有時這被當作Java的一個嚴重缺點,即使沒有任何真憑實據(jù)。
實證研究表明,如果數(shù)字數(shù)據(jù)(如價格波動)變化的頻率超過200毫秒一次,人就無法正常感知了。
應用主要是給人用的,因此我們有一個有用的經(jīng)驗法則,200毫秒或低于200毫秒的Stop-The-World(STW)通常是沒有影響的。有些應用可能有更高的要求(如流媒體),但很多GUI應用是不需要的。
少數(shù)應用(比如低延遲交易或機械控制系統(tǒng))無法接受200毫秒的停頓。除非編寫的就是這類應用,否則用戶基本感覺不到垃圾收集器的影響。
值得一提的是,在應用線程數(shù)量超過物理核數(shù)的任何系統(tǒng)中,操作系統(tǒng)必須控制對CPU的分時訪問。Stop-The-World聽著可怕,但實際上任何應用(不管是JVM還是其他應用)都要面對稀缺計算資源的爭用問題。
如果不去測量,JVM對應用性能有何附加影響是不清楚的。
總之,請打開GC日志,以此來確定停頓時間是否真的影響了應用。通過分析日志來確定停頓時間,這里既可以手工分析,也可以利用腳本或工具分析。然后再判定它們是否真的給應用于帶來了問題。最重要的是,問自己一個關鍵的問題:確實有用戶抱怨嗎?
7.手寫對象池適合一大類應用認為Stop-The-World停頓在某種程度上是不好的,應用開發(fā)團隊的一個常見反應就是在Java堆內(nèi)實現(xiàn)自己的內(nèi)存管理技術。這往往會歸結(jié)為實現(xiàn)一個對象池(甚至是全面的引用計數(shù)),而且需要使用了領域?qū)ο蟮娜魏未a都參與進來。
這種技術幾乎總是具有誤導性的。它基于過去的認知,那時對象分配非常昂貴,而修改對象則廉價的多。現(xiàn)在的情況已經(jīng)完全不同了。
現(xiàn)在的硬件在分配時非常高效;最新的桌面或服務器硬件,內(nèi)存帶寬至少是2到3GB。這是一個很大的數(shù)字,除非專門編寫的應用,否則要充分利用這么大的帶寬還真不容易。
一般來說,正確實現(xiàn)對象池非常困難(尤其是有多個線程工作時),而且對象池還帶來了一些負面的要求,使這種技術不是一個通用的良好選擇:
所有接觸到對象池代碼的開發(fā)者必須了解對象池,而且能正確處理哪些代碼知道對象池,哪些代碼不知道對象池,其界限必須讓大家知道,并且寫在文檔中這些額外的復雜性要保持更新,而且定期復審如果有一條不滿足,悄然出現(xiàn)問題(類似于C 中的指針復用)的風險就又回來了總之,只有GC停頓不能接受,而且調(diào)校和重構也未能將停頓減小到可以接受的水平時,才能使用對象池。
8.在垃圾收集中,相對于Parallel Old,CMS總是更好的選擇Oracle JDK默認使用一個并行的Stop-The-World收集器來收集老年代,即Parallel Old收集器。
Concurrent-Mark-Sweep (CMS)是一個備選方案,在大部分垃圾收集周期,它允許應用線程繼續(xù)運行,但這是有代價的,而且有一些注意事項。
允許應用線程與垃圾收集線程一起運行,不可避免地帶來一個問題:應用線程修改了對象圖,可能會影響對象的存活性。這種情況必須在事后加以清理,因此CMS實際上有兩個STW階段(通常非常短)。
這會帶來一些后果:
必須將所有應用線程帶到安全點,每次Full GC期間會停頓兩次;盡管垃圾收集與應用同時執(zhí)行,但應用的吞吐量會降低(通常是50%);在使用CMS進行垃圾收集時,JVM所用的簿記信息(和CPU周期)遠高于其他的并行收集器。這些代價是不是物有所值,取決于應用的情況。但是天下沒有免費的午餐。CMS收集器在設計上值得稱道,但它不是萬能的。
所以在確定CMS是正確的垃圾收集策略之前,首先應該確認Parallel Old的STW停頓確實不能接受,而且已經(jīng)無法調(diào)校。最后,我重點強調(diào)一下,所有指標必須從與生產(chǎn)系統(tǒng)等價的系統(tǒng)中獲得。
9.增加堆的大小可以解決內(nèi)存問題當應用陷入困境,并且懷疑是GC的問題時,很多應用團隊的反應就是增加堆的大小。在某些情況下,這樣做可以快速見效,而且為我們留出了時間來考慮更周詳?shù)慕鉀Q方案。然而,如果沒有充分理解性能問題的原因,這種策略反而會讓事情變得更糟糕。
考慮一個編碼非常糟糕的應用程序,它正在產(chǎn)生很多領域?qū)ο?(它們的生存時間很有代表性,比如說是2-3秒)。如果分配率高到一定程度,垃圾收集會頻繁進行,這樣領域?qū)ο髸惶嵘嚼夏甏nI域?qū)ο髱缀跏且贿M入年老代,生存時間就結(jié)束了,從而直接死亡,但它們直到下一次Full GC時才會被回收。
如果增加了應用的堆大小,我們所做的不過是增加了相對短命的對象進入和死亡所用的空間。這會導致Stop-The-World停頓時間更長,對應用并無益處。
在修改堆大小或者調(diào)校其他參數(shù)之前,理解對象的分配和生存時間的動態(tài)是很有必要的。沒有測量性能數(shù)據(jù)就盲目行動,只會使情況更糟糕。在這里,垃圾收集器的老年代分布情況特別重要。
結(jié)論當談到Java的性能調(diào)校時,直覺常常起誤導作用。我們需要實驗數(shù)據(jù)和工具來幫助我們將平臺的行為可視化并加強理解。
垃圾收集就是最好的例子。對于調(diào)校或者生成指導調(diào)校的數(shù)據(jù)而言,GC子系統(tǒng)擁有無限的潛力;但是對于產(chǎn)品應用而言,不使用工具很難理解所產(chǎn)生數(shù)據(jù)的意義。
默認情況下,運行任意Java進程(包括開發(fā)環(huán)境和產(chǎn)品環(huán)境),應該至少總是使用如下參數(shù):
-verbose:gc(打印GC日志)-Xloggc:(更全面的GC日志)-XX:+PrintGCDetails(更詳細的輸出)-XX:+PrintTenuringDistribution(顯示JVM所使用的將對象提升進入老年代的年齡閾值)
然后使用工具來分析日志,這里可以利用手寫的腳本,可以用圖生成,還可以使用GCViewer(開源的)或jClarity Censum這樣的可視化工具。
相關文章:
1. 解決vue頁面刷新,數(shù)據(jù)丟失的問題2. python 讀txt文件,按‘,’分割每行數(shù)據(jù)操作3. python logging.info在終端沒輸出的解決4. vue路由分文件拆分管理詳解5. vue+vuex+axios從后臺獲取數(shù)據(jù)存入vuex,組件之間共享數(shù)據(jù)操作6. 詳解android adb常見用法7. SpringBoot使用Captcha生成驗證碼8. android studio實現(xiàn)簡單的計算器(無bug)9. android 控件同時監(jiān)聽單擊和雙擊實例10. Python 忽略文件名編碼的方法
