JVM性能優(yōu)化,Java的伸縮性
很多程序員在解決JVM性能問題的時候,花開了很多時間去調(diào)優(yōu)應(yīng)用程序級別的性能瓶頸,當(dāng)你讀完這本系列文章之后你會發(fā)現(xiàn)我可能更加系統(tǒng)地看待這類的問題。我說過JVM的自身技術(shù)限制了Java企業(yè)級應(yīng)用的伸縮性。首先我們先列舉一些主導(dǎo)因素。
l 主流的硬件服務(wù)器提供了大量的內(nèi)存
l 分布式系統(tǒng)有大量內(nèi)存的需求,而且該需求在持續(xù)增長
l 一個普通Java應(yīng)用程序所持有的對空間大概在1GB~4GB,這遠遠低于一個硬件服務(wù)器的內(nèi)存管理能力以及一個分布式應(yīng)用程序的內(nèi)存需求量。這被稱之為Java內(nèi)存墻,如下圖所示(圖中表述Java應(yīng)用服務(wù)器和常規(guī)Java應(yīng)用的內(nèi)存使用量的演變史)。
圖 1 Java內(nèi)存墻(1980~2010)
Java內(nèi)存墻
這給我們帶來了如下JVM性能課題:
1) 如果分配給應(yīng)用程序的內(nèi)存太小,將導(dǎo)致內(nèi)存不足。JVM 不能及時釋放內(nèi)存空間給應(yīng)用程序,最終將引發(fā)內(nèi)存不足,或者JVM完全關(guān)閉。所以你必須提供更多的內(nèi)存給應(yīng)用程序。
2) 如果給對響應(yīng)時間敏感的應(yīng)用增加內(nèi)存,如果不重啟你的系統(tǒng)或者優(yōu)化你的應(yīng)用,Java堆最終會碎片化。當(dāng)碎片發(fā)生時,可能會導(dǎo)致應(yīng)用中斷100毫秒~100秒,這取決與你的Java應(yīng)用,Java堆的大小以及其他的JVM調(diào)優(yōu)參數(shù)。
關(guān)于停頓的討論大部分都集中在平均停頓或者目標停頓,很少涉及到堆壓縮時的最壞停頓時間,在生產(chǎn)環(huán)境中堆中每千兆字節(jié)的有效數(shù)據(jù)的都將會發(fā)生大約1秒的停頓。
2~4秒的停頓對大多數(shù)企業(yè)應(yīng)用來說都是不能接受的,所以盡管實際的Java應(yīng)用實例可能需要更多的內(nèi)存空間,但實際只分配2~4GB的內(nèi)存。在一 些64位系統(tǒng)中帶有很多關(guān)于伸縮性的JVM調(diào)優(yōu)項,使得這些系統(tǒng)可以運行16GB乃至20GB的堆空間,并能滿足典型響應(yīng)時間的SLA。但是這些離現(xiàn)實較 遠,JVM目前的技術(shù)無法在進行堆壓縮時避免停頓應(yīng)用程序。Java應(yīng)用開發(fā)人員苦于處理這兩個為我們大多數(shù)人所抱怨的任務(wù)。
l 架構(gòu)/建模在大量的實例池之上,隨之而來的是復(fù)雜的監(jiān)控和管理操作。
l 反復(fù)的JVM和應(yīng)用程序調(diào)優(yōu)以避免“stop the world“引起的停頓。大多數(shù)程序員希望停頓不要發(fā)生在系統(tǒng)峰值負載期間。我稱之為不可能的目標。
現(xiàn)在讓我們深入一點Java的可伸縮性問題。
過度供給或過度實例化Java部署
為了充分利用內(nèi)存資源,普通的做法是將Java應(yīng)用部署在多個應(yīng)用服務(wù)器實例上而不是一個或者少數(shù)應(yīng)用服務(wù)器實例上。當(dāng)一臺Server上運行16 個應(yīng)用服務(wù)器實例可以充分利用所有的內(nèi)存資源,但如此無法解決的是多實例的監(jiān)控以及管理所帶來的成本,尤其是當(dāng)你的應(yīng)用部署在多個Server上。
另一個問題來了,峰值負載時的內(nèi)存資源不是每天都需要的,這樣就形成了巨大的浪費。有些情況下,一臺物理機上可能只不是不超過3個“大應(yīng)用服務(wù)器實例”,這樣的部署更加不夠經(jīng)濟也不夠環(huán)保,尤其在非峰值負載期間。
讓我們來比較一下這兩種部署架構(gòu),下圖中左邊是多而小的應(yīng)用服務(wù)器實例部署模式,右邊是少而大的應(yīng)用服務(wù)器實例部署模式。兩種模式處理同樣的負載,究竟哪一種部署架構(gòu)更具經(jīng)濟性。
圖2 大應(yīng)用服務(wù)器部署場景
上圖源自:Azul Systems
如我之前說過的,并發(fā)壓縮使得大應(yīng)用服務(wù)器部署模式變得可行,而且可以突破JVM可伸縮性的限制。目前只有Azul的Zing JVM可以提供并發(fā)壓縮的技術(shù),另外Zing是Server側(cè)的JVM,我們很樂意看到越來越多的開發(fā)者在JVM層面去挑戰(zhàn)Java可伸縮性的問題。
由于性能調(diào)優(yōu)仍然是我們解決Java可伸縮性問題的主要手段,我們先來看有哪些主要的調(diào)優(yōu)參數(shù)以及通過它們能達到什么樣的效果。
調(diào)優(yōu)參數(shù):一些事例
最著名的調(diào)優(yōu)參數(shù)莫過于”-Xmx”了,通過該參數(shù)可以指定Java的堆空間大小,實際上可能不同的JVM執(zhí)行結(jié)果不太一樣。
有的JVM包含了內(nèi)部結(jié)構(gòu)(如編譯器線程,垃圾回收器結(jié)構(gòu),代碼緩存等等)所需要的內(nèi)存在“-Xmx”的設(shè)定中,而有的則不包含。因此用戶Java進程的大小不一定跟“-Xmx”的設(shè)定相吻合。
如果你的應(yīng)用程序分配對象的速率,對象的生命周期,或者對象的大小超過了JVM內(nèi)存相關(guān)配置,一旦達到最大可使用內(nèi)存的閾值將會發(fā)生內(nèi)存溢出,用戶進程則會停止。
當(dāng)你的應(yīng)用程序糾結(jié)于內(nèi)存的可用性時,最有效的方法就是通過”-Xmx”指定更大的內(nèi)存去重啟當(dāng)前應(yīng)用進程。為了避免頻繁的重啟,大多數(shù)企業(yè)生產(chǎn)環(huán)境都傾向于指定峰值負載時所需要的內(nèi)存,造成過度配置優(yōu)化。
提示:生產(chǎn)環(huán)境負載的調(diào)整
Java開發(fā)人員易犯的常見錯誤是在實驗下的做的堆內(nèi)存設(shè)置,在移植到生產(chǎn)環(huán)境是忘記重新調(diào)整。生產(chǎn)環(huán)境和實驗室環(huán)境是不一樣的,謹記根據(jù)生產(chǎn)環(huán)境的負載重新調(diào)整堆內(nèi)存設(shè)置。
#p#
分代垃圾回收器調(diào)優(yōu)
還有一些其他的優(yōu)化選項”-Xns”和”-XX: NewSize”,用來調(diào)整年輕代的大小,用來指定堆中專門負責(zé)新對象分配的空間大小。
大多數(shù)開發(fā)者都試圖基于實驗室環(huán)境調(diào)整年輕代的大小,這意味著在生產(chǎn)負載下存在失敗的風(fēng)險。一般新生代的大小設(shè)置為堆大小的三分之一至二分之一左 右,但這不是一個準則,畢竟實際還要視應(yīng)用程序邏輯而定。因此最好先調(diào)查清楚年輕代到年老代的蛻變率以及年老代對象的大小,在此基礎(chǔ)上(確保年老代的大 小,年老代過小會頻繁促發(fā)GC導(dǎo)致內(nèi)存溢出錯誤)盡可能地調(diào)大年輕代的空間。
還有一個與年輕代相關(guān)的調(diào)優(yōu)項”-XX:SurvivorRatio”,該選項用來指定年輕代中對象的生命周期,超過指定時長相關(guān)對象將被移至年老 代。為了”正確”地設(shè)定該值,你需要知道年輕代空間回收的頻率,能夠估算到新對象在應(yīng)用程序進程中被引用的時長,同時也取決于分配率。
并發(fā)垃圾回收調(diào)優(yōu)
針對對停頓敏感的應(yīng)用,建議使用并發(fā)垃圾回收,雖然并行的辦法能夠帶來非常好的吞吐量基準測試分數(shù),但是并行GC不利于縮短響應(yīng)時間。并發(fā) GC 是目前唯一有效的實現(xiàn)一致性和最少“stop the world”中斷的方法。不同的JVM提供不同的并發(fā)GC的設(shè)定,Oracle JVM(hotspot)提供”-XX:+UseConcMarkSweepGC”,今后G1將成為Oracle JVM默認的并發(fā)垃圾回收器。
性能調(diào)優(yōu)并不是真正的解決辦法
或許你已經(jīng)注意到上文中在討論如何“正確“地設(shè)定調(diào)優(yōu)此參數(shù)時,我刻意在”正確“二字上加了雙引號。那是因為就我個人經(jīng)驗而言一旦涉及到性能參數(shù)調(diào) 優(yōu),就沒有嚴格意義上的正確設(shè)定。每一個設(shè)定值都是針對特定的場景。考慮到應(yīng)用場景會發(fā)生變化,JVM 性能調(diào)整充其量是一個權(quán)宜之計。
以堆的設(shè)置為例:如果2GB的堆可以應(yīng)對20萬并發(fā)用戶,但是可能不能應(yīng)付40萬的并發(fā)用戶。
我們再以”-XX:SurvivorRatio”為例:當(dāng)設(shè)定符合一個負載持續(xù)增長最高至每毫秒10000個交易的場景,當(dāng)壓力到達每毫秒50000個交易時又會發(fā)生什么呢?
大多數(shù)企業(yè)級應(yīng)用負載都是動態(tài)的,Java語言的動態(tài)內(nèi)存管理以及動態(tài)編譯等技術(shù)使得Java更加適合企業(yè)級應(yīng)用。我們來看看一下兩個配置清單。
清單1. 應(yīng)用程序(1)的啟動選項
- >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. 應(yīng)用程序(2)的啟動選項
- >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 …
兩者的配置區(qū)別很大,因為他們是兩個不同應(yīng)用程序。感覺根據(jù)各自的應(yīng)用特設(shè)都做了”正確“的配置與調(diào)優(yōu)。在實驗室環(huán)境下都運行良好,但在生產(chǎn)環(huán)境中 最終會表現(xiàn)出疲態(tài)。清單1由于沒有考慮到動態(tài)負載,到了生產(chǎn)環(huán)境即表現(xiàn)不良。清單2沒有考慮到應(yīng)用程序在生產(chǎn)環(huán)境中的特性變化。這兩種情況應(yīng)該歸咎于開發(fā) 團隊,但是該歸咎于何處呢?
變通辦法可行嗎?
有些企業(yè)通過精確測量交易對象的大小定義極致的對象回收空間并”精簡“其架構(gòu)來適配該空間。這也許是辦法來削減碎片以應(yīng)對一整天的交易(在不做堆壓 縮的情況下)。還有一個辦法就是通過程序設(shè)計確保對象被引用的時間在一個比較短的時間內(nèi)從而阻止其在SurvivorRatio時間之后不被遷往年老代而 直接被回收,避免內(nèi)存壓縮的場景。這兩種辦法都可以,但是對應(yīng)用開發(fā)人員和設(shè)計人員有一定的挑戰(zhàn)。
誰保障應(yīng)用程序的性能?
一個門戶應(yīng)用可能會在其活動負載峰值點出現(xiàn)故障;一個交易應(yīng)用可能會在每次市場下跌和上升時無法正常運行;電子商務(wù)網(wǎng)站可能會無法應(yīng)對節(jié)假日購物高 峰期。這些都是真實世界的案例基本都是JVM性能參數(shù)調(diào)優(yōu)導(dǎo)致的。當(dāng)產(chǎn)生了經(jīng)濟損失,開發(fā)團隊就會受到責(zé)備。也許某些場合下開發(fā)團隊應(yīng)該要受到責(zé)備,但是 JVM的提供商又應(yīng)該負起什么樣兒的責(zé)任呢?
首先JVM提供商應(yīng)該要提供調(diào)優(yōu)參數(shù)的優(yōu)先順序,至少這在短期內(nèi)還是很有意義的。有一些新的調(diào)優(yōu)選項是針對特定的、 新興的企業(yè)應(yīng)用程序場景。更多的調(diào)優(yōu)選項是為了減輕JVM支持團隊的工作負荷而將性能優(yōu)化轉(zhuǎn)嫁到應(yīng)用開發(fā)者身上。但我個人認為這或?qū)?dǎo)致更加漫長的支持負 荷,一些針對最糟糕場景的調(diào)優(yōu)選項也將被延期,當(dāng)然不是無限延期。
毋庸置疑JVM的開發(fā)團隊也在努力地進行著他們的工作,同時也只有應(yīng)用實施者才會更加清楚他們應(yīng)用的特定需求。但是應(yīng)用的實施者或開發(fā)者是無法預(yù)測 期動態(tài)的負載需求。在過去,JVM提供商也會去分析關(guān)于Java的性能與可擴展性問題,哪些是他們能夠解決的。不是提供調(diào)優(yōu)參數(shù),而是直接去優(yōu)化或創(chuàng)新垃 圾回收的算法。更有趣是我們可以想象一下如果OpenJDK的社區(qū)聚集在一起重新考慮Java垃圾回收器將會發(fā)生什么!
JVM性能的基準測試
調(diào)優(yōu)參數(shù)有時被JVM提供商作為其競爭的工具,因為不同的調(diào)優(yōu)可以改善他們的JVM在可預(yù)見的環(huán)境中的性能表現(xiàn),本系列的最后一片文章中將調(diào)查這些基準測試來衡量JVM的性能。
JVM開發(fā)者的挑戰(zhàn)
真正的企業(yè)級可伸縮性需求是要求JVM能夠適應(yīng)動態(tài)靈活的應(yīng)用負載。這是在特定吞吐量和響應(yīng)時間內(nèi)保證持續(xù)穩(wěn)定性能的關(guān)鍵。這是JVM開發(fā)者才能完成歷史使命,因此是時候號召我們Java開發(fā)者社區(qū)來迎接真正的Java可伸縮性的挑戰(zhàn)。
- l 持續(xù)調(diào)優(yōu)
對于給定的應(yīng)用,在一開始需要告知其需要多大的內(nèi)存,之后的工作都應(yīng)該有JVM來負責(zé) ,JVM需要適配動態(tài)的應(yīng)用負載和運行場景。
- l JVM實例數(shù) vs. 實例的可擴展性
現(xiàn)在的服務(wù)器都支持很大的內(nèi)存,那么為什么JVM實例不能有效地利用它呢?將應(yīng)用拆分部署許多小的應(yīng)用服務(wù)器實例上,這從經(jīng)濟和環(huán)保角度都是一種浪費?,F(xiàn)代的JVM需要跟上硬件和應(yīng)用的發(fā)展潮流。
- l 真實世界的性能和可伸縮性
企業(yè)不需要為其應(yīng)用的性能需求去做極致的性能調(diào)優(yōu)。JVM提供商和OpenJDK社區(qū)需要去解決Java可伸縮性的核心問題以及消除“stop the world“的操作。
結(jié)論
如果JVM做了這樣的工作,并且提供了并發(fā)壓縮的垃圾回收算法,JVM也不再成為Java可伸縮性的限制因素,Java應(yīng)用開發(fā)者不需要花費痛苦的 時間理解怎樣配置JVM去獲得最佳性能,從而將會有更多的有趣的Java應(yīng)用層面的創(chuàng)新,而不是無休止的JVM調(diào)優(yōu)。我要挑戰(zhàn)JVM開發(fā)人員以及提供商所 需要做的事情來響應(yīng)甲骨文所提倡的“Make the Java Future“的活動。
關(guān)于作者
Eva Andearsson對JVM技術(shù)、SOA、云計算和其他企業(yè)級中間件解決方案有著10多年的從業(yè)經(jīng)驗。在2001年,她以JRockit JVM開發(fā)者的身份加盟了創(chuàng)業(yè)公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收領(lǐng)域的研究和算法方面,EVA獲得了兩項專利。此外她還是提出了確定性垃圾回收 (Deterministic Garbage Collection),后來形成了JRockit實時系統(tǒng)(JRockit Real Time)。在技術(shù)上,Eva與SUN公司和Intel公司合作密切,涉及到很多將JRockit產(chǎn)品線、WebLogic和Coherence整合的項 目。2009年,Eva加盟了Azul System公,擔(dān)任產(chǎn)品經(jīng)理。負責(zé)新的Zing Java平臺的開發(fā)工作。最近,她改換門庭,以高級產(chǎn)品經(jīng)理的身份加盟Cloudera公司,負責(zé)管理Cloudera公司Hadoop分布式系統(tǒng),致力于高擴展性、分布式數(shù)據(jù)處理框架的開發(fā)。
譯文鏈接:http://ifeve.com/jvm%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%EF%BC%8C-part-5%EF%BC%9Aja