JVM性能優化, Part 5:Java的伸縮性

ImportNew注: JVM性能優化系列文章前4篇由ImportNew翻譯(第一篇,第二篇,第三篇, 第四篇)。本文由新浪微博:吳傑 (@WildJay) 投稿至ImportNew。感謝吳傑! 若是你但願分享好的原創文章或者譯文,歡迎投稿到ImportNew。java


不少程序員在解決JVM性能問題的時候,花開了不少時間去調優應用程序級別的性能瓶頸,當你讀完這本系列文章以後你會發現我可能更加系統地看待這類的問題。我說過JVM的自身技術限制了Java企業級應用的伸縮性。首先咱們先列舉一些主導因素。程序員


l  主流的硬件服務器提供了大量的內存算法


l  分佈式系統有大量內存的需求,並且該需求在持續增加緩存


l  一個普通Java應用程序所持有的對空間大概在1GB~4GB,這遠遠低於一個硬件服務器的內存管理能力以及一個分佈式應用程序的內存需求量。這被稱之爲Java內存牆,以下圖所示(圖中表述Java應用服務器和常規Java應用的內存使用量的演變史)。性能優化


Java內存牆(1980~2010)   (圖片來源:Azul Systems)服務器

Java內存牆(1980~2010)   (圖片來源:Azul Systems)架構

這給咱們帶來了以下JVM性能課題:併發


1)    若是分配給應用程序的內存過小,將致使內存不足。JVM 不能及時釋放內存空間給應用程序,最終將引起內存不足,或者JVM徹底關閉。因此你必須提供更多的內存給應用程序。分佈式


2)    若是給對響應時間敏感的應用增長內存,若是不重啓你的系統或者優化你的應用,Java堆最終會碎片化。當碎片發生時,可能會致使應用中斷100毫秒~100秒,這取決與你的Java應用,Java堆的大小以及其餘的JVM調優參數。工具


關於停頓的討論大部分都集中在平均停頓或者目標停頓,不多涉及到堆壓縮時的最壞停頓時間,在生產環境中堆中每千兆字節的有效數據的都將會發生大約1秒的停頓。


2~4秒的停頓對大多數企業應用來講都是不能接受的,因此儘管實際的Java應用實例可能須要更多的內存空間,但實際只分配2~4GB的內存。在一些64位系統中帶有不少關於伸縮性的JVM調優項,使得這些系統能夠運行16GB乃至20GB的堆空間,並能知足典型響應時間的SLA。可是這些離現實較遠,JVM目前的技術沒法在進行堆壓縮時避免停頓應用程序。Java應用開發人員苦於處理這兩個爲咱們大多數人所抱怨的任務。


l  架構/建模在大量的實例池之上,隨之而來的是複雜的監控和管理操做。


l  反覆的JVM和應用程序調優以免「stop the world「引發的停頓。大多數程序員但願停頓不要發生在系統峯值負載期間。我稱之爲不可能的目標。


如今讓咱們深刻一點Java的可伸縮性問題。


過分供給或過分實例化Java部署


爲了充分利用內存資源,普通的作法是將Java應用部署在多個應用服務器實例上而不是一個或者少數應用服務器實例上。當一臺Server上運行16個應用服務器實例能夠充分利用全部的內存資源,但如此沒法解決的是多實例的監控以及管理所帶來的成本,尤爲是當你的應用部署在多個Server上。


另外一個問題來了,峯值負載時的內存資源不是天天都須要的,這樣就造成了巨大的浪費。有些狀況下,一臺物理機上可能只不是不超過3個「大應用服務器實例」,這樣的部署更加不夠經濟也不夠環保,尤爲在非峯值負載期間。


讓咱們來比較一下這兩種部署架構,下圖中左邊是多而小的應用服務器實例部署模式,右邊是少而大的應用服務器實例部署模式。兩種模式處理一樣的負載,究竟哪種部署架構更具經濟性。


大應用服務器部署場景 (圖片來源:Azul Systems)

大應用服務器部署場景 (圖片來源:Azul Systems)

如我以前說過的,併發壓縮使得大應用服務器部署模式變得可行,並且能夠突破JVM可伸縮性的限制。目前只有Azul的Zing JVM能夠提供併發壓縮的技術,另外Zing是Server側的JVM,咱們很樂意看到愈來愈多的開發者在JVM層面去挑戰Java可伸縮性的問題。


因爲性能調優仍然是咱們解決Java可伸縮性問題的主要手段,咱們先來看有哪些主要的調優參數以及經過它們能達到什麼樣的效果。


調優參數:一些事例


最著名的調優參數莫過於」-Xmx」了,經過該參數能夠指定Java的堆空間大小,實際上可能不一樣的JVM執行結果不太同樣。


有的JVM包含了內部結構(如編譯器線程,垃圾回收器結構,代碼緩存等等)所須要的內存在「-Xmx」的設定中,而有的則不包含。所以用戶Java進程的大小不必定跟「-Xmx」的設定相吻合。


若是你的應用程序分配對象的速率,對象的生命週期,或者對象的大小超過了JVM內存相關配置,一旦達到最大可以使用內存的閾值將會發生內存溢出,用戶進程則會中止。


當你的應用程序糾結於內存的可用性時,最有效的方法就是經過」-Xmx」指定更大的內存去重啓當前應用進程。爲了不頻繁的重啓,大多數企業生產環境都傾向於指定峯值負載時所須要的內存,形成過分配置優化。


提示:生產環境負載的調整


Java開發人員易犯的常見錯誤是在實驗下的作的堆內存設置,在移植到生產環境是忘記從新調整。生產環境和實驗室環境是不同的,謹記根據生產環境的負載從新調整堆內存設置。


分代垃圾回收器調優


還有一些其餘的優化選項」-Xns」和」-XX: NewSize」,用來調全年輕代的大小,用來指定堆中專門負責新對象分配的空間大小。


大多數開發者都試圖基於實驗室環境調全年輕代的大小,這意味着在生產負載下存在失敗的風險。通常新生代的大小設置爲堆大小的三分之一至二分之一左右,但這不是一個準則,畢竟實際還要視應用程序邏輯而定。所以最好先調查清楚年輕代到年老代的蛻變率以及年老代對象的大小,在此基礎上(確保年老代的大小,年老代太小會頻繁促發GC致使內存溢出錯誤)儘量地調大年輕代的空間。


還有一個與年輕代相關的調優項」-XX:SurvivorRatio」,該選項用來指定年輕代中對象的生命週期,超過指定時長相關對象將被移至年老代。爲了」正確」地設定該值,你須要知道年輕代空間回收的頻率,可以估算到新對象在應用程序進程中被引用的時長,同時也取決於分配率。


併發垃圾回收調優


針對對停頓敏感的應用,建議使用併發垃圾回收,雖然並行的辦法可以帶來很是好的吞吐量基準測試分數,可是並行GC不利於縮短響應時間。併發 GC 是目前惟一有效的實現一致性和最少「stop the world」中斷的方法。不一樣的JVM提供不一樣的併發GC的設定,Oracle JVM(hotspot)提供」-XX:+UseConcMarkSweepGC」,從此G1將成爲Oracle JVM默認的併發垃圾回收器。


性能調優並非真正的解決辦法


或許你已經注意到上文中在討論如何「正確「地設定調優此參數時,我刻意在」正確「二字上加了雙引號。那是由於就我我的經驗而言一旦涉及到性能參數調優,就沒有嚴格意義上的正確設定。每個設定值都是針對特定的場景。考慮到應用場景會發生變化,JVM 性能調整充其量是一個權宜之計。


以堆的設置爲例:若是2GB的堆能夠應對20萬併發用戶,可是可能不能應付40萬的併發用戶。


咱們再以」-XX:SurvivorRatio」爲例:當設定符合一個負載持續增加最高至每毫秒10000個交易的場景,當壓力到達每毫秒50000個交易時又會發生什麼呢?


大多數企業級應用負載都是動態的,Java語言的動態內存管理以及動態編譯等技術使得Java更加適合企業級應用。咱們來看看一下兩個配置清單。


清單1. 應用程序(1)的啓動選項


1

2

3

4

5

6

>java -Xmx12g -XX:MaxPermSize=64M -XX:PermSize=32M -XX:MaxNewSize=2g 

-XX:NewSize=1g -XX:SurvivorRatio=16 -XX:+UseParNewGC 

-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=0 

-XX:CMSInitiatingOccupancyFraction=60 -XX:+CMSParallelRemarkEnabled 

-XX:+UseCMSInitiatingOccupancyOnly -XX:ParallelGCThreads=12 

-XX:LargePageSizeInBytes=256m …

清單2. 應用程序(2)的啓動選項


1

2

3

4

5

>java --Xms8g --Xmx8g --Xmn2g -XX:PermSize=64M -XX:MaxPermSize=256M 

-XX:-OmitStackTraceInFastThrow -XX:SurvivorRatio=2 -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC 

-XX:+CMSConcurrentMTEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelSurvivorRemarkEnabled 

-XX:CMSMaxAbortablePrecleanTime=10000 -XX:+UseCMSInitiatingOccupancyOnly 

-XX:CMSInitiatingOccupancyFraction=63 -XX:+UseParNewGC --Xnoclassgc …

二者的配置區別很大,由於他們是兩個不一樣應用程序。感受根據各自的應用特設都作了」正確「的配置與調優。在實驗室環境下都運行良好,但在生產環境中最終會表現出疲態。清單1因爲沒有考慮到動態負載,到了生產環境即表現不良。清單2沒有考慮到應用程序在生產環境中的特性變化。這兩種狀況應該歸咎於開發團隊,可是該歸咎於何處呢?


變通辦法可行嗎?


有些企業經過精確測量交易對象的大小定義極致的對象回收空間並」精簡「其架構來適配該空間。這也許是辦法來削減碎片以應對一成天的交易(在不作堆壓縮的狀況下)。還有一個辦法就是經過程序設計確保對象被引用的時間在一個比較短的時間內從而阻止其在SurvivorRatio時間以後不被遷往年老代而直接被回收,避免內存壓縮的場景。這兩種辦法均可以,可是對應用開發人員和設計人員有必定的挑戰。


誰保障應用程序的性能?


一個門戶應用可能會在其活動負載峯值點出現故障;一個交易應用可能會在每次市場下跌和上升時沒法正常運行;電子商務網站可能會沒法應對節假日購物高峯期。這些都是真實世界的案例基本都是JVM性能參數調優致使的。當產生了經濟損失,開發團隊就會受到責備。也許某些場合下開發團隊應該要受到責備,可是JVM的提供商又應該負起什麼樣兒的責任呢?


首先JVM提供商應該要提供調優參數的優先順序,至少這在短時間內仍是頗有意義的。有一些新的調優選項是針對特定的、 新興的企業應用程序場景。更多的調優選項是爲了減輕JVM支持團隊的工做負荷而將性能優化轉嫁到應用開發者身上。但我我的認爲這或將致使更加漫長的支持負荷,一些針對最糟糕場景的調優選項也將被延期,固然不是無限延期。


毋庸置疑JVM的開發團隊也在努力地進行着他們的工做,同時也只有應用實施者纔會更加清楚他們應用的特定需求。可是應用的實施者或開發者是沒法預測期動態的負載需求。在過去,JVM提供商也會去分析關於Java的性能與可擴展性問題,哪些是他們可以解決的。不是提供調優參數,而是直接去優化或創新垃圾回收的算法。更有趣是咱們能夠想象一下若是OpenJDK的社區彙集在一塊兒從新考慮Java垃圾回收器將會發生什麼!


JVM性能的基準測試


調優參數有時被JVM提供商做爲其競爭的工具,由於不一樣的調優能夠改善他們的JVM在可預見的環境中的性能表現,本系列的最後一片文章中將調查這些基準測試來衡量JVM的性能。


JVM開發者的挑戰


真正的企業級可伸縮性需求是要求JVM可以適應動態靈活的應用負載。這是在特定吞吐量和響應時間內保證持續穩定性能的關鍵。這是JVM開發者才能完成歷史使命,所以是時候號召咱們Java開發者社區來迎接真正的Java可伸縮性的挑戰。


l  持續調優


對於給定的應用,在一開始須要告知其須要多大的內存,以後的工做都應該有JVM來負責 ,JVM須要適配動態的應用負載和運行場景。


l  JVM實例數 vs. 實例的可擴展性


如今的服務器都支持很大的內存,那麼爲何JVM實例不能有效地利用它呢?將應用拆分部署許多小的應用服務器實例上,這從經濟和環保角度都是一種浪費。現代的JVM須要跟上硬件和應用的發展潮流。


l  真實世界的性能和可伸縮性


企業不須要爲其應用的性能需求去作極致的性能調優。JVM提供商和OpenJDK社區須要去解決Java可伸縮性的核心問題以及消除「stop the world「的操做。


結論


若是JVM作了這樣的工做,而且提供了併發壓縮的垃圾回收算法,JVM也再也不成爲Java可伸縮性的限制因素,Java應用開發者不須要花費痛苦的時間理解怎樣配置JVM去得到最佳性能,從而將會有更多的有趣的Java應用層面的創新,而不是無休止的JVM調優。我要挑戰JVM開發人員以及提供商所須要作的事情來相應甲骨文所提倡的「Make the Java Future「的活動。

相關文章
相關標籤/搜索