《JVM故障診斷指南》之2 —— 調整合適的Java堆大小的技巧
在生產係統上決定合適的Java堆大小不是一個容易的操作。許多性能問題的發生都是由於不恰當的Java堆容量的錯誤調整。這部分將從介紹一些技巧作為開頭,它能幫助你在當前的或者新的生產係統上決定最佳的Java堆大小。其中一些技巧對預防OutOfMemoryError問題和內存泄露方麵也同樣有用。
請注意這些技巧是傾向於“幫助你”決定合適的Java堆大小。因為每一個IT環境都不相同,實際上你是處於最好的時機來精確決定你客戶環境所需要的Java堆參數。
1 – JVM: 你總是擔心你不理解的部分
你如何期望去配置,調整和故障診斷那些你不懂的部分?你也許從沒有機會去調整和改善Java虛擬機參數,但是為了提高你的知識以及故障診斷能力,你仍然可以免費的學習它們的基礎。 也許你不同意,但是從我的觀點來說,那種Java程序員不需要了解內部JVM內存管理的想法是錯誤的。
Java堆調整和故障處理對Java和JavaEE初學者來說尤其是一個挑戰,下麵是一個典型的情形 :
• 你的客戶生產環境經常麵對OutOfMemoryError 問題,並導致影響了許多業務。你的支持團隊對於解決這些問題有很大壓力。
• 通過快速Google搜索,你能找到類似問題的例子並認為(以為)你麵對的是同樣的問題。
• 此時你從另一個同樣發生了OutOfMemoryError 情況的JVM裏拿到-Xms和-Xmx的值,並希望能快速解決客戶的問題。
• 然後你對你的環境進行了處理並實現了同樣的調整。2天過後,你發現問題依然存在(甚至更糟糕或者情況好一點),你隻能繼續鬥爭下去…
問題出在哪?
• 你沒有首先找到問題的根源並對它有合適的了解。
• 同樣你也沒有對你生產環境上的更深層次的情況(比如參數規範,負載情況等)有合適的了解。網絡搜索是一個很好的可以學到並分享知識的方法,但是你還是需要去做嚴格的調查以及問題根源的分析。
• 你缺少一些JVM及其內存管理方式的基礎知識,這阻礙了你從更多維度獲得信息並整合。
我給你的 #1 技巧和建議是去學習和理解JVM不同內存空間的基礎規則,這種知識很關鍵,它能使你給客戶做出有用的建議,並能適度的了解後期調整策略可能出現的影響和危險。
再次提醒,Java虛擬機內存被分為3個部分:
• Java 堆: 應用於所有JVM提供商,通常被分為年輕代 (nursery)和年老代(tenured)。
• 持久代: 僅應用於Sun HotSpot虛擬機 (持久代會在之後的Java更新中被移除)
• 本地堆(C堆): 應用於所有JVM提供商。
如你所見,Java虛擬機內存管理遠遠複雜於通過-Xmx設置最大的可能值。你應該從各個角度來看,包括你的本地區和持久代的內存需求,它需要基於主機的物理可用內存(以及cpu核心數)。
對於32位JVM而言比較棘手,因為Java堆和本地堆在內存上是競爭關係。你的Java堆越大,本地堆就越小。嚐試去為32位虛擬機設置一個大內存比如2.5G以上,根據你應用的內存占用和線程的數量,它會增大本地堆發生OutOfMemoryError 的危險性。64位虛擬機解決了這些問題,但它仍然限製於物理可用資源以及垃圾收集開銷(major gc成本隨著空間變大而增加)。下麵這圖說明內存更大並不總是好事,所以你不要以為你可以在一個16G機器 64位JVM進程上運行20個Java EE應用。
2 – 數據和應用為主:重新檢查你的靜態數據占用需求
你的應用以及相關的數據會決定Java堆的占用需求。通過靜態內存,也就是說“可預計”的內存需求要遵循下麵每條:
• 確定你要部署多少個應用到單獨的JVM進程中,比如多個EAR文件,WAR文件,jar文件等。你在單個JVM裏部署的越多,本地堆的內存需求越高。
• 確定有多少潛在的Java類文件在運行時會被加載,包括第三方API。類在運行時加載越多,Hotspot虛擬機的持久代內存需求越高,內部JIT相關的優化對象也越多。
• 確定數據緩存占用,舉個例子,由應用程序(和第三方API)加載作為內部緩存的數據結構,如從數據庫拿到的作為緩存的數據,從文件讀取的數據等。你用的緩存數據越多,Java堆老年代空間的內存需求也越高。
• 確定你的中間件允許創建的線程數。這點很重要,因為Java線程需要足夠的本地內存,否則會發生OutOfMemoryError 。
舉個例子,如果你需要部署10個不同的EAR應用到單獨的JVM進程中,和2個或3個應用相比,你需要很多本地內存和持久代內存。數據緩存沒有被序列化到磁盤或者數據庫將需要占用額外的老年代內存。
盡量計算出靜態內存占用需要的合理值。這對於在你真正開始測量操作之前設置一些起始的JVM容量數字非常有用。對於32位JVM,我通常不建議Java堆大小超過2Gb(-Xms2048m, -Xmx2048m),因為你需要為你的Java EE應用和線程在持久代和本地堆分配足夠的內存。
這種分配對於在32位JVM進程中部署太多應用並且很容易導致本地堆內存消耗過大而言尤其有用。特別是在多線程環境中。
對於64位而言,我通常建議在每個JVM進程中Java堆大小的起始值設置在3 GB 或者 4 GB。
3 – 業務流量設置規則:重新檢查你的動態占用內存需求
業務流量通常會決定你動態內存占用。並發的用戶和請求會產生JVM“心跳”是因為有非常頻繁的對象被創建以及對於短生命周期,長生命周期對象的頻繁垃圾收集,你可以用多種監控工具來觀察。從上麵的JVM圖表可以看出,典型的年輕代和老年代的比例是1:3或者33%。
對於典型的32位虛擬機,Java堆設置到2Gb(使用分代&並發收集器)通常會分配500M給年輕代,1.5G給老年代。
減少major gc的頻率是性能優化的關鍵,所以,了解和估算在峰值期間需要多少內存是很重要的。
同樣的,你的應用和數據的類型會決定你需要的內存大小。購物車類型的應用(長時間存活對象)涉及大的非序列化的會話數據,通常需要很大的Java堆和很大的老年代空間。無狀態和XML處理為主的應用(很多短時間存活對象) 為了減少major收集的頻率需要合適的年輕代空間。
比如:
• 你有5個EAR應用(超過2000個類)部署(也包括中間件代碼)。
• 你的本地堆需求被預估在1Gb(需要足夠大去處理線程創建)。
• 你的持久代空間預估在512 MB。
• 你的內部靜態數據緩存預估在500MB.
• 你在高峰期預估的流量每小時總共有5000個並發用戶。
• 每個用戶會話數據占用預估在500 K。
• 整個會話數據的占用需求在高峰期有2.5 GB。
如你所見,像這種需求是不可能將所有的流量都放在單個32位JVM進程上。一個典型的處理方法是分離這些流量到幾個JVM進程上或者物理機上(假定你有足夠的硬件和CPU核數可用)。
然而,對於這個例子,給了這麼多內存的要求還要保證一個可擴展的環境能長時間運行,我會建議用64位虛擬機,並用小一點的堆作為初始值比如3Gb來減小GC成本。如果你確定要給老年代額外的緩存空間,我通常會建議內存占用達到50%的時候再交給major收集,這是為了維持低的Full GC頻率和足夠的緩存應對容錯事件。
大多數時間,你的業務流量會用掉你大部分的內存,除非你需要用大批的數據緩存來達到合適的性能,這通常用於大型門戶(媒體)應用。太多的數據緩存也會產生很多的警告,你也許在之後需要重新審視和設計。
4 – 不要猜測,要測量
在這裏你應該:
• 了解JVM規則和內存空間的基礎知識。
• 對所有應用以及他們的特點(大小,類型,動態流量,無狀態vs有狀態對象,內部緩存等等)有很深的認識和理解。
• 對你的每一個應用的業務流量(比如並發用戶等)有很好的了解和前瞻預估。
• 對是否需要64位的虛擬機有自己的看法,並且知道初始參數設置。
• 對是否需要更多JVM(中間件)進程有自己的看法。
但是等一下,你的工作還沒完。盡管上麵的信息對於你想出“最好的推測”Java堆設置是很關鍵和有用的,但最好的建議還是在你的真實應用上模擬操作並且通過合適的分析,負載和性能測試去驗證Java堆內存的需求。
你可以學習和利用一些工具如JProfiler。我自己的觀點是,學習如何使用分析工具是最好的方法,它能使你恰當的了解應用中的內存占用的。另一種方法我經常用在生產線上的是堆轉儲分析,可以用Eclipse MAT工具。堆轉儲分析是非常有用的,能讓你對整個堆內存占用有一個全局認識和理解,包括類加載相關的數據,它在任何內存占用分析中都是必做的操作,尤其是內存泄露。
Java分析器和堆轉儲分析工具可以使你了解和確認你應用程序的內存占用,包括內存泄露的檢測和解決。負載和性能測試也是必做的,通過模擬前期並發用戶它可以幫你驗證之前的預測。同時也能暴露你應用程序的瓶頸以及使你更好的調整JVM參數設置。你可以用類似Apache JMeter工具,它簡單易學,當然也可以使用一些其他的商業化的產品。
最後,我經常看到Java EE環境能運行得非常好,直到有一天基礎設施開始出現問題比如硬件問題。很快運行環境中各種指標數量持續的下降 (JVM進程減少),然後整個環境開始崩潰。到底發生了什麼?
有許多情形會導致連環影響,但是缺少JVM調優和缺乏處理容錯(短期額外負載)的能力是最常見的。如果你的JVM進程運行在老年代空間裏對象占比80%+的狀態下並且頻繁的垃圾收集,你如何期望它能處理任何容錯情況?
你可以在前期進行模擬這種情形下的負載性能測試,這樣就可以調整合適的設置,你的Java堆要有足夠的緩存來處理短期內額外的負載(多餘的對象)。這對於動態內存占用是最主要也是最適用的方式,因為容錯意味著將某個百分比的並發用戶轉發到其他可用的JVM進程(如中間件)上去。
5 – 劃分與戰勝
到這裏,你已經完成了幾十次的負載迭代測試。你知道你的JVM沒有泄露內存。你的應用程序占用也不會繼續降低了。你試過好幾種調整策略比如使用64位Java堆並設置10G以上內存,以及多種GC策略,但是仍然沒有找到合適的可接受的性能指標?
在我的經驗中,我發現,基於當前的JVM參數規格,在每個物理主機上創建幾個JVM進程,並跨越多個主機,這種適當的橫向和縱向伸縮性可以帶給你需要的吞吐量和容量。如果在幾個邏輯點關掉一些應用(有自己的JVM進程,線程和調優參數),你的IT環境會有更好的容錯性。
這種“劃分與戰勝”策略將你的應用流量分到多個JVM進程中,它能帶給你:
• 每個JVM進程(包括靜態和動態占用)減少了Java堆大小
• 降低了JVM調優的複雜性
• 降低了每個JVM進程消耗和暫停的時間
• 增強了冗餘和容錯能力
• 能和最新的雲端IT虛擬化策略保持一致
總結下,當你發現你花了太多時間在64位JVM上調優,是時候重新審視你的中間件和JVM部署策略,並充分利用橫向縱向伸縮性的優點。完成這些策略雖然很費勁,但是卻能給你長時間運行帶來好結果。
譯者介紹:
梅小西
Java工程師,關注JVM,並發編程,喜歡研究Python,Scala,Golang等。
最後更新:2017-05-22 15:34:45