【轉發】關於Java性能的9個謬論

Java的性能有某種黑魔法之稱。部分緣由在於Java平臺很是複雜,不少狀況下問題難以定位。然而在歷史上還有一種趨勢,人們靠智慧和經驗來研究Java性能,而不是靠應用統計和實證推理。在這篇文章中,我但願拆穿一些最荒謬的技術神話。html

1.Java很慢

關於Java的性能有不少謬論,這一條是最過期的,可能也是最爲明顯的。java

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

然而從那之後,虛擬機和JIT技術已經有了十多年的改進,Java的總體性能如今已經很是好了。web

在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平臺的其餘意想不到的方面。

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

只有那些有實際需求的開發者和開發團隊才應該編寫微基準測試。這些基準測試應該徹底公開(包括源代碼),並且是能夠復現的,還應接受同行評審及進一步的審查。

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停頓在某種程度上是很差的,應用開發團隊的一個常見反應就是在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這樣的可視化工具。

【支持原創】轉發自:http://www.codeceo.com/article/9-error-java-performance.html

相關文章
相關標籤/搜索