《筆者帶你剖析大規(guī)模分布式Java平臺JVM性能調(diào)優(yōu)基礎(chǔ)》
?
前言
其實說到對JVM進(jìn)行性能調(diào)優(yōu)早已是一個老生常談的話題,如果你所在的技術(shù)團(tuán)隊還暫時達(dá)不到淘寶團(tuán)隊那樣的高度,無法滿足在OpenJDK的基礎(chǔ)之上根據(jù)自身業(yè)務(wù)進(jìn)行針對性的 二次開發(fā) 和 定制調(diào)優(yōu) ,那么對于你來說,唯一的選擇就是盡可能的熟悉JVM的內(nèi)存布局,以及熟練掌握與GC相關(guān)的那些選項配置,否則JVM的基礎(chǔ)性能調(diào)優(yōu)不是 癡人說夢 ?
?
目錄
一、性能調(diào)優(yōu)的一些概念和目標(biāo);
二、性能調(diào)優(yōu)的基本原則;
三、新生代的性能調(diào)優(yōu);
四、老年代的性能調(diào)優(yōu);
?
一、性能調(diào)優(yōu)的一些概念和目標(biāo)
相信對JVM有所了解的開發(fā)人員,對于調(diào)優(yōu)過程中牽扯的 吞吐性、低延遲/高響應(yīng) 應(yīng)該不會感覺到陌生。既然生產(chǎn)環(huán)境中是大規(guī)模的分布式Java平臺,JVM吃的內(nèi)存必然不會太少。不知大家是否還曾記得,64位的JVM能夠順利訪問大內(nèi)存,其最主要的原因是因為其采用了64位的指針架構(gòu),這同時也是尋址訪問大內(nèi)存的關(guān)鍵要素。而與之相反的32位的JVM的內(nèi)存卻被限定在了2-3GB上限(與操作平臺密切相關(guān),Linux平臺,Windows則為1.5G上限)。
?
大規(guī)模的分布式Java平臺除了JVM吃的內(nèi)存特別大外(筆者之前的項目單點持有內(nèi)存為30GB),為了增加每一個節(jié)點的可用性,都是采用多 JVM集群的部署模式 ,這樣一來一旦發(fā)生單點故障的時候,不會導(dǎo)致整個服務(wù)不可用,從而也能夠降低單點負(fù)載,提升整體程序的執(zhí)行性能,更好的滿足一些特定的高并發(fā)場景。
?
話說生產(chǎn)部署在服務(wù)器上的JVM大都是主動或者缺省選擇server模式在奔跑,并且在Java7版本之后,JVM缺省開啟了 分層編譯(Tiered Compilation)策略 ,由C1和C2編譯器共同來執(zhí)行本地代碼的編譯任務(wù),C1編譯器會對字節(jié)碼進(jìn)行簡單和可靠的優(yōu)化,以達(dá)到更快的編譯速度,而C2編譯器則會啟動一些耗時更長的優(yōu)化,以獲取更好的本地代碼編譯質(zhì)量。
?
那么對JVM進(jìn)行性能調(diào)優(yōu)的真正目的是什么呢?簡單來說就是為了滿足程序的高吞吐量、低延遲/高響應(yīng)性等需求。但是筆者不得不提醒大家, 調(diào)優(yōu)是一個循序漸進(jìn)的過程,必然需要經(jīng)歷多次迭代,最終才能換取一個較好的折中方案 。筆者在《Java虛擬機(jī)精講》中曾經(jīng)提及過,垃圾收集器中吞吐量和低延遲這兩個目標(biāo)其實是存在相互競爭的矛盾,因為如果選擇以吞吐量優(yōu)先,那么降低內(nèi)存回收的執(zhí)行頻率則是必然的,但這將會導(dǎo)致GC需要更長的暫停時間來執(zhí)行內(nèi)存回收。相反如果是選擇以低延遲優(yōu)先,那么為了降低每次執(zhí)行內(nèi)存回收時的暫停時間,只能夠頻繁地執(zhí)行內(nèi)存回收,但這又引起了新生代內(nèi)存的縮減和導(dǎo)致程序吞吐量的下降。舉個例子,在60s的JVM總運(yùn)行時間里,每次GC的執(zhí)行頻率是20s/次,那么60s內(nèi)一共會執(zhí)行3次內(nèi)存回收,按照每次GC耗時100ms來計算,最終一共會有300ms(即60/20*100)被用于執(zhí)行內(nèi)存回收。但是如果我們將選項“-XX:MaxGCPauseMillis”的值調(diào)小后,新生代的內(nèi)存空間也會自動調(diào)整,相信大家都知道,內(nèi)存空間越小就越容易被耗盡,那么GC的執(zhí)行頻率就會更頻繁。之前在60s的JVM總運(yùn)行時間里,最終會有300ms被用于執(zhí)行內(nèi)存回收,而如今GC的執(zhí)行頻率卻是10s/次,60s內(nèi)將會執(zhí)行6次內(nèi)存回收,按照每次GC耗時80ms來計算,雖然看上去暫停時間更短了,但最終一共會有480ms(即60/10*80)被用于執(zhí)行內(nèi)存回收,很明顯程序的吞吐量下降了。 因此,在JVM調(diào)優(yōu)這個領(lǐng)域,沒有任何一種調(diào)優(yōu)方案是適用于所有應(yīng)用場景的,同時,切勿極端才能夠達(dá)到JVM性能調(diào)優(yōu)的真正目的和意義 。
?
? 二、性能調(diào)優(yōu)的基本原則
簡而言之,總而言之,對JVM進(jìn)行性能調(diào)優(yōu)時,有2個基本原則大家需要進(jìn)行理解。首先是 盡可能的讓GC發(fā)生在新生代中 ,也就是盡可能的多執(zhí)行Minor GC,因為我們都知道Full GC的執(zhí)行頻率盡管不會有Minor GC那么頻繁,但是對程序響應(yīng)性的影響是非常大的(筆者之前的項目Full GC詭異般的執(zhí)行了50s,顯然超出了對響應(yīng)延遲的容忍度)。那么多讓Minor GC執(zhí)行,顯然可以 減少觸發(fā)Full GC的頻率 。
?
其次, GC所持有的可用內(nèi)存越大 (Java Heap所占有的堆空間越大), GC的執(zhí)行效率越好 。這是因為內(nèi)存越大, 達(dá)到回收閾值就越不容易 ,那么明顯可以提升程序的吞吐量和響應(yīng)性。當(dāng)然這并不是說越大越好,如果一個項目JVM撐死只需要1-2G的運(yùn)行內(nèi)存,人傻錢多分配120G的內(nèi)存量,或許程序在穩(wěn)定情況下運(yùn)行到硬件故障也不會發(fā)生一次Full GC。
?
既然內(nèi)存并不是越大越好,總有一個閾值。這就牽扯到生產(chǎn)環(huán)境中,開發(fā)人員究竟應(yīng)該如何對Heap分配初始大小?其實這很簡單,一個經(jīng)歷過嚴(yán)謹(jǐn)測試的項目,必然會在測試環(huán)境中測試N個周期才會移交至生產(chǎn)環(huán)境中進(jìn)行部署,那么在測試環(huán)境中,我們可以根據(jù)多次迭代后觀察Full GC的數(shù)據(jù)信息來 估算 生產(chǎn)環(huán)境中究竟應(yīng)該給我們的項目初始多大的內(nèi)存空間。比如經(jīng)過多次迭代后,F(xiàn)ull GC產(chǎn)生的數(shù)據(jù)信息中,如果老年代中的活躍數(shù)據(jù)占用內(nèi)存大小為100m,那么按照通用的計算法則,可以按照約 3-4倍 的占用倍數(shù)來恒定生產(chǎn)環(huán)境中應(yīng)該分配的堆大小(即-Xms和-Xmx),新生代和老年代的比例官方建議按照整個堆的3/8來進(jìn)行分配,也就是說選項-Xmn可以占用整個堆內(nèi)存空間的3/8,這是一種非常簡單和通用的計算和分配方式。而永久代則可以按照Full GC后產(chǎn)生的數(shù)據(jù)信息,根據(jù)永久代活躍數(shù)據(jù)占用內(nèi)存大小的 1.5倍 進(jìn)行恒定生產(chǎn)環(huán)境中應(yīng)該分配的初始值。
?
這里筆者稍微補(bǔ)充一下,在一些高并發(fā)場景下,尤其關(guān)注吞吐量和高響應(yīng)的應(yīng)用中,應(yīng)該將-Xms和-Xmx設(shè)定為同一值,以此 避免內(nèi)存動態(tài)調(diào)整時產(chǎn)生的Full GC操作 ,永久代-XX:PermSize和-XX:MaxPermSize同理。
?
三、新生代的性能調(diào)優(yōu)
在HotSpot中,串行回收GC與并行回收GC是2個極端,在如今,更多人更傾向于選擇后者,并且在一些極其注重吞吐量和高響應(yīng)的應(yīng)用場景下, 并行回收有著串行回收無法比擬的絕對優(yōu)勢 。由于堆空間中的對象大部分都是一些 瞬時對象 ,因此這類對象的生命周期往往更多是由新生代進(jìn)行“控制”,之前也說過,盡可能的讓垃圾收集動作發(fā)生在新生代中,而不是Full GC。這樣一來,對于新生代的性能調(diào)優(yōu)就主要集中在幾個問題上,首先是測量出Minor GC的執(zhí)行平率和持續(xù)時間是否滿足需求,以及-XX:ParallelGCThreads選項的配置。如圖A-1所示:
?
如果說Minor GC執(zhí)行的太頻繁,那么必然是-Xmn分配得過小,反之Minor GC很久才執(zhí)行一次,而每次執(zhí)行的周期較長,則意味著-Xmn分配得過大。那么究竟應(yīng)該如何對新生代進(jìn)行調(diào)優(yōu)呢?簡單來說,我們需要多次迭代,從最初將-Xmn的值設(shè)置到最低,然后 逐步微調(diào) ,慢慢的你會發(fā)現(xiàn)Minor GC的執(zhí)行頻率在降低,直到最終滿足需求即可停止。經(jīng)過這樣的調(diào)試,你會發(fā)現(xiàn)程序的吞吐量上來了,但是每次執(zhí)行Minor GC的周期會變得較長,怎么辦呢?我們可以通過-XX:ParallelGCThreads選項調(diào)整GC執(zhí)行的線程數(shù),讓更多的GC線程執(zhí)行垃圾收集,提升GC的回收效率。這樣一來, 基本可以滿足降低GC的回收平率,提升GC的回收效率 。
?
由于使用的是并行GC,我們可以 充分利用多核CPU資源以及線程資源 。同微調(diào)-Xmn選項一樣,我們首先可以將-XX:ParallelGCThreads設(shè)置為物理CPU核心數(shù)的1/2,比如你的CPU是6核,那么-XX:ParallelGCThreads的值就可以設(shè)置為3(最好不要小于2,否則將會影響并行GC的回收效率),這樣一來,CPU可用資源就會將一半分配給GC線程使用,而剩下的CPU資源則服務(wù)于應(yīng)用線程中。當(dāng)然如果你的項目并不重視高響應(yīng),-XX:ParallelGCThreads的值可以相對的進(jìn)行減少,以便于有更多的CPU資源分配給程序中的工作線程。
?
四、老年代的性能調(diào)優(yōu)
新生代的調(diào)優(yōu)如果大家都已經(jīng)掌握,接下來我們再來看老年代如何進(jìn)行性能調(diào)優(yōu)。盡管調(diào)優(yōu)原則中筆者提及過,應(yīng)該讓垃圾收集動作盡可能的發(fā)生在新生代中,也就是盡可能多執(zhí)行Minor GC,但是這 并不代表程序永遠(yuǎn)不會執(zhí)行Full GC ,一旦程序觸發(fā)Full GC時,所花費(fèi)的時間往往要大于Minor GC的執(zhí)行周期,如果Full GC執(zhí)行的周期過長,對用戶所帶來的直觀感受是非常不友好的,比如用戶在執(zhí)行登錄操作,恰恰悲催的碰見JVM正在執(zhí)行長時間的Full GC,請自行補(bǔ)白。。。
?
在GC的命令選項中并不存在直接設(shè)置來年代內(nèi)存大小的選項,那么老年代的內(nèi)存大小如何設(shè)置呢?簡單來說,老年代的內(nèi)存空間大小間接等于-Xmx的值減去-Xmn的值,比如-Xmx為120G,-Xmn的值為45G,那么剩下的75G就是老年代的內(nèi)存空間。在此大家需要注意,如果當(dāng)-Xmn產(chǎn)生變化時,-Xmx也要隨之成比例的發(fā)生變化,否則老年代占用的內(nèi)存空間將會增大或變小,如果增大,F(xiàn)ull GC的執(zhí)行周期將會變得更長,反之執(zhí)行頻率將會頻繁。
?
一般來說,如果<=3G以下的堆內(nèi)存,建議使用的GC組合是Parallel和Parallel Old,除非真的是需求無法容忍系統(tǒng)出現(xiàn)長時間的“Stop the World”(目前幾乎沒有任何一款GC不需要暫停工作線程,只是盡可能的縮短暫停時間,包括G1)情況下,才推薦上CMS,不過一般大內(nèi)存的使用,老年代首推CMS執(zhí)行垃圾收集,并且CMS也是除G1之外的HotSpot中唯一的一款可以 單獨(dú)執(zhí)行老年代增量回收 ,而不必執(zhí)行Full GC全量回收的垃圾收集器(Promotion Failed和Concurrent Mode Failed情況除外)。
?
之所以要用CMS,是因為CMS天生為低延遲/高響應(yīng)而生。因為CMS的執(zhí)行過程中,只有初始標(biāo)記和再次標(biāo)記會出現(xiàn)暫停,而其它過程CMS的工作線程將會和程序的工作線程同時工作,大大提升了GC的回收效率。那么使用CMS同樣需要進(jìn)行優(yōu)化,其中最主要的就是調(diào)整-Xmx的大小和-XX:CMSInitiatingOccupancyFraction選項。如圖A-2所示:
?
-XX:CMSInitiatingOccupancyFraction用于設(shè)置老年代中的內(nèi)存使用率達(dá)到多少百分比的時候執(zhí)行內(nèi)存回收(低版本的JDK缺省值為68%,JDK6及以上版本缺省值則為92%),在JDK6以后續(xù)版本中,如果按照缺省配置,當(dāng)老年代的內(nèi)存使用率達(dá)到92%后才進(jìn)行垃圾收集,這往往會導(dǎo)致從新生代晉升到老年代中的對象將 無法進(jìn)行存放 ,如果-XX:CMSInitiatingOccupancyFraction設(shè)置得太低又會導(dǎo)致CMS GC 觸發(fā)的頻率太快 。一般來說,在大內(nèi)存的堆使用上,筆者將這個值設(shè)置在70-80之間算是比較合理的。
?
盡管CMS是大內(nèi)存的首選,但是CMS仍然是有一些令人不滿意的地方,比如搶占CPU資源、內(nèi)存碎片等問題。不過總而言之,CMS目前在大內(nèi)存的使用上,仍然是首選。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
