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

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