關於Java效能的9個謬論

來源:互聯網
上載者:User

標籤:

Java的效能有某種黑魔法之稱。部分原因在於Java平台非常複雜,很多情況下問題難以定位。然而在曆史上還有一種趨勢,人們靠智慧和經驗來研究Java效能,而不是靠應用統計和實證推理。在這篇文章中,我希望拆穿一些最荒謬的技術神話。

1.Java很慢

關於Java的效能有很多謬論,這一條是最過時的,可能也是最為明顯的。

確實,在上世紀90年代和本世紀初處,Java有時是很慢。

然而從那以後,虛擬機器和JIT技術已經有了十多年的改進,Java的整體效能現在已經非常好了。

在6個獨立的Web效能基準測試中,Java架構在24項測試中有22項位列前四。

儘管JVM利用效能剖析僅最佳化常用的代碼路徑,但這種最佳化效果很明顯。很多情況下,JIT編譯的Java代碼和C++一樣快,而且這樣的情況越來越多了。

儘管如此,依然有人認為Java平台很慢,這或許源自體驗過Java平台早期版本的人的曆史偏見。

在下結論之前,我們建議保持客觀的態度,並且評估一下最新的效能結果。

2.可以孤立地看待單行Java代碼

考慮下面這行短小的代碼:

MyObject obj = new MyObject();

對Java開發人員而言,看似很明顯,這行代碼一定會分配一個對象並調用適當的構造器。

我們也許可以據此推出效能邊界了。我們認為這行代碼一定會導致執行一定量的工作,基於這種推定,就可以嘗試計算其效能影響了。

其實這種認識是錯誤的,它讓我們先入為主地認為,不管什麼工作,在任何情況下都會進行。

事實上,javac和JIT編譯器都能夠將無作用程式碼最佳化掉。就JIT編譯器而言,基於效能剖析資料,甚至可以通過預測將代碼最佳化掉。在這樣的情況下,這行代碼根本不會運行,所以不會影響效能。

此外,在某些JVM中——比如JRockit——JIT編譯器甚至可以將對象上的操作分解,這樣即便代碼路徑還有效,分配操作也可以避免。

這裡的寓意是,在處理Java效能問題時,上下文非常重要,過早的最佳化有可能產生違反直覺的結果。所以最好不好過早最佳化。相反,應該總是構建代碼,並且使用效能調校技術來定位效能熱點,然後加以改進。

3.微基準測試和你想象的一樣

正如我們上面看到的那樣,檢查一小段代碼不如分析應用的整體效能來的準確。

儘管如此,開發人員還是喜歡編寫微基準測試。似乎對平台底層的某些方面進行修修補補會帶來無窮的樂趣。

理查德·費曼曾經說過:“不要欺騙自己,你自己正是最容易被欺騙的人。”這句話用來說明編寫Java微基準測試這件事是再合適不過了。

編寫良好的微基準測試極其困難。Java平台非常複雜,而且很多微基準測試只能用於測量瞬時效應,或是Java平台的其他意想不到的方面。

例如,如果沒有經驗,編寫的微基準測試往往就是測一下時間或垃圾收集,卻沒有抓住真正的影響因素。

只有那些有實際需求的開發人員和Team Dev才應該編寫微基準測試。這些基準測試應該完全公開(包括原始碼),而且是可以複現的,還應接受同行評審及進一步的審查。

Java平台的很多最佳化表明統計運行和單次運行對結果影響很大。要得到真實可靠的答案,應該將一個單獨的基準測試回合多次,然後把結果匯總到一起。

如果讀者感覺有必要編寫微基準測試,Georges、Buytaert和Eeckhout等人的論文《利用嚴格的統計方法評測Java 效能(Statistically Rigorous Java Performance Evaluation)》是個不錯的開始。缺乏適當的統計分析,我們很容易被誤導。

有很多開發好的工具以及圍繞這些工具的社區(比如Google的Caliper)。如果確實有必要編寫微基準測試,那也不要自己編寫,這時需要的是同行的意見和經驗。

4.演算法慢是效能問題的最常見原因

在開發人員之間有一個很常見的認知錯誤(普通福士也是如此),即認為系統中他們控制的那部分很重要。

在探討Java效能時,這種認知錯誤也有所體現:Java開發人員認為演算法的品質是效能問題的主要原因。開發人員考慮的是代碼,因此他們自然會偏向於考慮自己的演算法。

實際上在處理一系列現實中的效能問題時,人們發現演算法設計是根本問題的幾率不足10%。

相反,與演算法相比,垃圾收集、資料庫訪問和配置錯誤導致應用程式緩慢的可能性更大。

大部分應用處理的資料量相對較小,因此,即使主要演算法效率不高,通常也不會導致嚴重的效能問題。可以肯定,我們的演算法不是最優的;儘管如此,演算法帶來的效能問題還是算小的,更多效能問題是應用棧的其他部分導致的。

因此我們的最佳建議是,使用實際生產資料來揭開效能問題的真正原因。要測量效能資料,而不是憑空猜測!

5.緩衝可以解決所有問題

“電腦科學中的所有問題都可以通過引入一個中介層來解決。”

David Wheeler的這句程式員格言(在互連網上,這句話至少還被認為是其他兩位電腦科學家說的)非常常見,尤其是在Web開發人員之中很流行。

如果未能透徹理解現有的架構,而且分析也已停頓,往往就是“緩衝可以解決所有問題”這種謬論抬頭的時候了。

在開發人員看來,與其處理嚇人的現有系統,還不如在前面加一層緩衝,將現有系統隱藏起來,以此期待最好的情況。無疑,這種方式只是讓整體架構更複雜了,當下一個接手的開發人員打算瞭解系統現狀時,情況會更糟糕。

規模龐大、設計拙劣的系統往往缺乏整體的設計,是一次一行代碼、一個子系統這樣寫出來的。然而很多情況下,簡化並重構架構會帶來更好的效能,而且幾乎總是更容易讓人理解。

所以當評估是否真的有必要加入緩衝時,應該先計劃收集一些基本的使用統計資訊(比如命中率和未命中率等),以此證明緩衝層帶來的真正價值。

6.所有應用都需要關注Stop-The-World問題

Java平台存在一個無法改變的事實:為運行垃圾收集,所有應用線程必須周期性停頓。有時這被當作Java的一個嚴重缺點,即使沒有任何真憑實據。

實證研究表明,如果數字資料(如價格波動)變化的頻率超過200毫秒一次,人就無法正常感知了。

應用主要是給人用的,因此我們有一個有用的經驗法則,200毫秒或低於200毫秒的Stop-The-World(STW)通常是沒有影響的。有些應用可能有更高的要求(如流媒體),但很多GUI應用是不需要的。

少數應用(比如低延遲交易或機械控制系統)無法接受200毫秒的停頓。除非編寫的就是這類應用,否則使用者基本感覺不到垃圾收集器的影響。

值得一提的是,在應用線程數量超過物理核心數的任何系統中,作業系統必須控制對CPU的分時訪問。Stop-The-World聽著可怕,但實際上任何應用(不管是JVM還是其他應用)都要面對稀缺計算資源的爭用問題。

如果不去測量,JVM對應用效能有何附加影響是不清楚的。

總之,請開啟GC日誌,以此來確定停頓時間是否真的影響了應用。通過分析日誌來確定停頓時間,這裡既可以手工分析,也可以利用指令碼或工具分析。然後再判定它們是否真的給應用於帶來了問題。最重要的是,問自己一個關鍵的問題:確實有使用者抱怨嗎?

7.手寫對象池適合一大類應用

認為Stop-The-World停頓在某種程度上是不好的,應用Team Dev的一個常見反應就是在Java堆內實現自己的記憶體管理技術。這往往會歸結為實現一個對象池(甚至是全面的引用計數),而且需要使用了領域對象的任何代碼都參與進來。

這種技術幾乎總是具有誤導性的。它基於過去的認知,那時對象分配非常昂貴,而修改對象則廉價的多。現在的情況已經完全不同了。

現在的硬體在分配時非常高效;最新的案頭或伺服器硬體,記憶體頻寬至少是2到3GB。這是一個很大的數字,除非專門編寫的應用,否則要充分利用這麼大的頻寬還真不容易。

一般來說,正確實現對象池非常困難(尤其是有多個線程工作時),而且對象池還帶來了一些負面的要求,使這種技術不是一個通用的良好選擇:

  • 所有接觸到對象池代碼的開發人員必須瞭解對象池,而且能正確處理
  • 哪些代碼知道對象池,哪些代碼不知道對象池,其界限必須讓大家知道,並且寫在文檔中
  • 這些額外的複雜性要保持更新,而且定期複審
  • 如果有一條不滿足,悄然出現問題(類似於C 中的指標複用)的風險就又回來了

總之,只有GC停頓不能接受,而且調校和重構也未能將停頓減小到可以接受的水平時,才能使用對象池。

8.在垃圾收集中,相對於Parallel Old,CMS總是更好的選擇

Oracle JDK預設使用一個並行的Stop-The-World收集器來收集老年代,即Parallel Old收集器。

Concurrent-Mark-Sweep (CMS)是一個備選方案,在大部分垃圾收集周期,它允許應用線程繼續運行,但這是有代價的,而且有一些注意事項。

允許應用線程與垃圾收集線程一起運行,不可避免地帶來一個問題:應用線程修改了對象圖,可能會影響對象的存活性。這種情況必須在事後加以清理,因此CMS實際上有兩個STW階段(通常非常短)。

這會帶來一些後果:

  1. 必須將所有應用線程帶到安全點,每次Full GC期間會停頓兩次;
  2. 儘管垃圾收集與應用同時執行,但應用的輸送量會降低(通常是50%);
  3. 在使用CMS進行垃圾收集時,JVM所用的簿記資訊(和CPU周期)遠高於其他的並行收集器。

這些代價是不是物有所值,取決於應用的情況。但是天下沒有免費的午餐。CMS收集器在設計上值得稱道,但它不是萬能的。

所以在確定CMS是正確的垃圾收集策略之前,首先應該確認Parallel Old的STW停頓確實不能接受,而且已經無法調校。最後,我重點強調一下,所有指標必須從與生產系統等價的系統中獲得。

9.增加堆的大小可以解決記憶體問題

當應用陷入困境,並且懷疑是GC的問題時,很多應用團隊的反應就是增加堆的大小。在某些情況下,這樣做可以快速見效,而且為我們留出了時間來考慮更周詳的解決方案。然而,如果沒有充分理解效能問題的原因,這種策略反而會讓事情變得更糟糕。

考慮一個編碼非常糟糕的應用程式,它正在產生很多領域對象 (它們的存留時間很有代表性,比如說是2-3秒)。如果分配率高到一定程度,垃圾收集會頻繁進行,這樣領域對象會被提升到老年代。領域對象幾乎是一進入年 老代,存留時間就結束了,從而直接死亡,但它們直到下一次Full GC時才會被回收。

如果增加了應用的堆大小,我們所做的不過是增加了相對短命的對象進入和死亡所用的空間。這會導致Stop-The-World停頓時間更長,對應用並無益處。

在修改堆大小或者調校其他參數之前,理解對象的分配和存留時間的動態是很有必要的。沒有測量效能資料就盲目行動,只會使情況更糟糕。在這裡,垃圾收集器的老年代分布情況特別重要。

結論

當談到Java的效能調校時,直覺常常起誤導作用。我們需要實驗資料和工具來協助我們將平台的行為可視化並加強理解。

垃圾收集就是最好的例子。對於調校或者產生指導調校的資料而言,GC子系統擁有無限的潛力;但是對於產品應用而言,不使用工具很難理解所產生資料的意義。

預設情況下,運行任意Java進程(包括開發環境和產品環境),應該至少總是使用如下參數:

-verbose:gc(列印GC日誌)
-Xloggc:(更全面的GC日誌)
-XX:+PrintGCDetails(更詳細的輸出)
-XX:+PrintTenuringDistribution(顯示JVM所使用的將對象提升進入老年代的年齡閾值)

然後使用工具來分析日誌,這裡可以利用手寫的指令碼,可以用圖產生,還可以使用GCViewer(開源的)或jClarity Censum這樣的視覺化檢視。

關於Java效能的9個謬論

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.