關於 Java 效能方面的 9 個謬論

oschina發表於2013-04-28

英文原文:9 Fallacies of Java Performance  編譯:oschina

關於 Java 效能方面的 9 個謬論

Java效能問題被冠以某種黑暗魔法的稱謂。一部分是因為其平臺的複雜性,在很多情況下,無法定位其效能問題根源。然而,在以前對於Java效能的技巧,有一種趨向:認為其由人們的智慧,經驗構成,而不是應用統計和實證推理。在這篇文章中,我希望去驗證一些最荒謬的技術神話。

1. Java執行慢

在所有最過時的Java效能謬論當中,這可能是最明顯的言論。

是的,在90年代和20年代初期,Java確實有點慢。

然而,在那之後,我們有超過10年的時間來改進虛擬機器和JIT技術,現在Java整個體系的效能已經快的令人驚訝。

在6個單獨的web效能測試基準中,Java框架佔據了24個當中的22個前四的位置。

JVM效能分析元件的使用不僅優化了通用的程式碼路徑,而且在優化那些嚴重領域也很有成效。JIT編譯程式碼的速度在大多數情況下跟C++一樣快了。

儘管這樣,關於Java執行慢的言論還是存在,估計是由於歷史原因造成的偏見,這個偏見來自當時那些使用Java早期版本的人們。

我們建議,在匆忙下結論之前先保留意見和評估一下最新的效能結果。

2. 單行java程式碼意味著任何事都是孤立的

考慮以下一小段程式碼:

MyObject obj = new MyObject();

對一個Java開發者而言,很明顯能看出這行程式碼需要分配一個物件和執行一個對應的構造方法。

從這來看,我們可以開始推出效能邊界。我們知道有一些精確數量的工作必須繼續,因此基於我們的推測,我們可以計算出效能影響。

這兒有個認知偏見,那就是根據以往的經驗,任何工作都需要被做。

實際上,javac和JIT編譯器都可以優化無效程式碼。就拿JIT編譯器來說,程式碼甚至可以基於資料分析而被優化掉,在這種情況下,該行程式碼將不會被執行,因此它就不會有什麼效能方面的影響。

而且,在一些Java虛擬機器(JVM)中,例如JRockit中,即使程式碼路徑沒有完全失效,JIT編譯器為了避免分配物件甚至可以執行分解物件操作。

這段文字在這裡的意義就是當處理Java效能方面的問題時,上下文很重要。而且過早的優化可能會產生意料之外的結果。所以為了獲得最好的結果,不要試圖過早的優化。與其不斷構建你的程式碼不如用效能調整技術去定位並且改正程式碼效能的潛在危險區。

3. 一個微基準測試意味著你認為它是什麼

正如以上看到的,推理一小段程式比分析應用程式的整體效能更不準確。儘管如此,開發者還是喜歡寫為基準測試。有些人似乎從擺弄平臺的某些低層次方面獲取無窮無盡的內心快感。

理查德·費曼曾說:“第一個原則是,不要欺騙自己,而且自己是最容易被騙的人”。沒有比編寫Java為基準測試更切合這個的例子了。

寫好微基準測試是極其困難的。Java平臺很複雜,而且很多微基準測試只對測量瞬態效應或平臺的其他意外方面有效。

例如,一個想當然的微基準測試頻繁地在測量子系統時間或垃圾回收,而不是在試圖捕捉效果時結束。

只有當開發者和團隊有真正基準需求的時候才需要寫微基準測試。這些基準測試應該打包到專案(包括原始碼)隨專案一起釋出,並且這些基準測試應該是可重現的並可提供給他人審閱和進一步的審查。

Java平臺的許多優化結果都指的是隻執行單一基準測試用例時所得到的統計結果。一個單獨的基準測試必須要多次執行才能得到一個比較趨近於真實答案的結果。

如果你認為到了必須要寫微基準測試的時候,首先請讀一下Georges, Buytaert, Eeckhout所著的”Statistically Rigorous Java Performance Evaluation”。如果沒有統計學的知識,你會很容易誤入歧途的。

網上有很多好的工具和社群來幫助你進行基準測試,例如Google的Caliper。如果你不得不寫基準測試時,不要埋頭苦幹,你需要從他人那裡汲取意見和經驗。

4.演算法慢是效能問題的最普遍原因

在程式設計師(和普通大眾)中普遍存在一個錯誤觀點就是他們總是理所當然地認為自己所負責的那部分系統才是最重要的。

就Java效能這個問題來說,Java開發者認為演算法的質量是效能問題的主要原因。開發者會考慮如何編碼,因此他們本性上就會潛意識地去考慮演算法。

實際上,當處理現實中的效能問題時,演算法設計佔用瞭解決基本問題不到10%的時間。

相反,相對於演算法,垃圾回收,資料庫訪問和配置錯誤會更可能造成程式緩慢。

大多數應用程式處理相對少量的資料,因此即使主演算法有缺陷也不會導致嚴重的效能問題。因此,我們得承認演算法對於效能問題來說是次要的;因為演算法帶來的低效相對於其他部分造成的影響來說是相對較小的,大多的效能問題來自於應用程式棧的其他部分。

因此我們的最佳建議就是依靠經驗和產品資料來找到引起效能問題的真正原因。要動手採集資料而不是憑空猜測。

快取能解決一切問題

“關於電腦科學的每一個問題都可以通過附加另外一個層面間接的方式被解決”

這句程式設計師的格言,來至於David Wheeler(幸虧有因特網,至少有另外兩位電腦科學家),是驚人的相似,特別是在web開發者中。

通常出現這種謬論是因為當面對一個現有的,理解不夠透徹的架構時出現的分析癱瘓。

與其處理一個令人生畏的現存系統,開發者經常會選擇躲避它通過新增一個快取並且抱著最大的希望。當然,這個方法僅僅使整個架構變的更復雜,並且對試圖理解產品架構現狀的下一位開發者而言是一件很糟糕的事情。

誇大的說,不規則架構每次被寫入一行和一個子系統。然而,在許多情況下,更簡單的重構架構會有更好的效能,而且它們幾乎也更易於被理解。

因此當你評估是不是需要快取時,計劃去收集基本用法統計(缺失率,命中率等)去證明實際上快取層是個附加值。

6. 所有應用都要考慮到STW

(譯註:“stop-the-world” 機制簡稱STW,即,在執行垃圾收集演算法時,Java應用程式的其他所有除了垃圾收集幫助器執行緒之外的執行緒都被掛起)

Java平臺的一個存在事實是,所有應用執行緒必須週期性的停止以便讓垃圾蒐集器GC執行。這有時被誇大為嚴重的弱點,即使是在缺少真實證據的情況下。

實證研究已經說明,人類通常無法察覺到頻率超過每200毫秒一次的數字資料的變化(例如價格變動)。

因此對以人類作為首要使用者的應用,一條有用的經驗就是200毫秒或低於200毫秒的 Stop-The-World (STW)停頓通常無需考慮。有些應用(例如視訊流)需要比這個更低的GC波動,但是很多GUI應用不是的。

有少數應用(比如低延遲交易,或者機械控制系統)對200毫秒停頓是不可接受的。除非你的應用屬於那個少數,否則你的使用者察覺到任何由垃圾回收帶來的影響是不太可能的。

值得注意的是,在具有比物理核心更多應用執行緒的系統中,作業系統任務計劃將會干涉對CPU的時間分片訪問。Stop-The-World聽起來嚇人,但實際上,每個應用(無論是不是JVM)都必須處理對稀缺計算資源的內容訪問。

如果不做測量,JVM的方法對應用效能帶來的額外影響具有何等意義將無法看清。

總體來說,判斷停頓的次數實際對應用的影響是通過開啟GC日誌的辦法。分析此日誌(或者手工,或者用指令碼或工具)來確定停頓的次數。然後再判定這些是否確實給你的應用域帶來問題。最重要的是,問自己一個最尖銳的問題:有使用者確實抱怨了嗎?

7. 手工處理的物件池對很大範圍內的應用都是合適的

對Stop-The-World停頓的壞感覺引起一個常見的應對,即在java堆的範圍內,為應用程式組發明它們自己的記憶體管理技術。經常這會歸結為實現一個物件池(或甚至是全面引用計數)的方法,並且需要讓任何使用了領域物件的程式碼參與進來。

這種技術幾乎總是被誤導。它通常具有自身久遠以前的根源,那時物件定位代價昂貴,突然的變化被認為是不重要的。但現在的世界已經非常不同。

現代的硬體具有難以想象的定位效率;近來桌面或伺服器硬體的記憶體容量至少達到了2到3GB。這是一個很大的數字;除了專業的使用情形,讓實際的應用充滿那麼大的容量不是很容易。

物件池一般很難正確的實現(特別是有多個執行緒在工作的時候),並且有幾個消極的要求使得把它作為一般場景使用成為一個艱難選擇:

  • 所有接觸到程式碼的開發者都必須清楚物件池並正確的處理它
  • “對池清醒”程式碼與“對池不清醒”程式碼之間的界限必須要通曉並明文規定
  • 所有這些附加的複雜性必須保持最新,並定期評估
  • 如果這裡任何地方失敗了,無聲損壞的風險(類似C中的指標重用)將被再次引入

總之,只有在GC停頓不能被接受,而且在除錯與重構過程中聰明的嘗試也不能縮減停頓到可接受水平的時候,物件池才可以使用。

8. 在GC中CMS總是比Parallel Old更好

Oracle JDK預設使用一個並行的,全部停止(stop-the-world STW)垃圾收集器來收集老年代的垃圾。

另外一個選擇是併發標記清除(CMS)收集器。這個收集器允許程式執行緒在大部分的GC週期中仍然繼續工作,但它需要付出一些代價和帶來一些警告。

允許程式執行緒和GC執行緒一起執行不可避免地導致物件表的變異同時又影響到物件的活躍性。這不得不在發生後進行清楚,所以CMS實際上有兩個STW階段(通常非常短)。

這會帶來一些後果:

  1. 所有程式執行緒不得不放進一個安全點並且在每次完全收集時停止兩次;
  2. 在收集併發執行地同時,程式吞吐量會減少(通常是50%)
  3. 在JVM從事通過CMS來收集垃圾的總體資料上(包括CPU週期)比並行收集更加高的。

依據程式的情況這些成本或者是值得的或者又不是。但並沒有免費的午餐。CMS收集器是一個卓越的工程品,但它不是萬能藥。

所以在介紹前,CMS是你正確的GC策略,你得首先考慮Parallel Old的STW是不可接收的和不能調和的。最後,(我不能足夠地強調),確定所有的指標都從相當的生產系統上得到。

9. 增加堆記憶體會解決你記憶體溢位的問題

當一個應用程式崩潰,GC中止執行時,許多應用組會通過增加堆記憶體來解決問題。在許多情況下,這可以很快解決問題,並爭取時間來考慮出一個更深的解決方案。然而,在沒有真正理解效能產生的根源時,這種解決策略實際上會使情況更糟糕。

試想一下,一個編碼很爛的應用構造了非常多的領域物件(生命週期大概維持2,3秒)。如果記憶體分配率足夠高,垃圾回收就會很快地執行,並把這些領域物件放到年老代。一旦進入了老年代,物件就會立即死去,但直到下一次完全回收才會被垃圾回收器回收。

如果這個應用增加其堆記憶體,那麼我們能做的是增加空間,為了存放那些相對短期存在,然後消逝的領域物件。這會使得 Stop-The-World 的時間更長,對應用毫無益處。

在改變堆記憶體和或其他引數之前,理解一下物件的動態分配和生命週期是很有必要的。沒做調查就行動,只會使事情更糟。在這裡,垃圾回收器的老年分佈資訊是非常重要的。

總結

當說道Java效能調優時直覺通常會誤導人。我們需要經驗資料和工具來幫助我們具象化和了解平臺的特性。

垃圾收集也許提供了這方面最好的例子。GC子系統對於調優和生產資料指導調整有驚人的潛力,但對於生產程式它是很難去不借助工具來讓產生的資料有意義。

執行任何Java程式,預設都應該最少有這些標記:

-verbose:gc(列印GC日誌)

-Xloggc:(更全面的GC日誌)

-XX:+PringGCDetail(更詳細的輸出)

-XX:+PrintTenuringDistribution(顯示由JVM設定的保有閾值)

然後使用工具來分析日誌——手寫指令碼和一些生成圖,或一個視覺化工具如(開源的)GCViewer或JClarity Censum。

相關文章