筆者帶你剖析大規(guī)模分布式Java平臺JVM性能調(diào)優(yōu)

你了解JVM嗎?

你用過分布式嗎?

今天我們就來講講Java分布式以及性能調(diào)優(yōu)。 其實說到對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)不是癡人說夢

(閱讀完大概需要10分鐘,學(xué)海無涯)

目錄

一、性能調(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選項的配置。如圖所示:


如果說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時,所花費的時間往往要大于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中唯一的一款可以單獨執(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選項。如圖所示:


-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之間算是比較合理的。

如果你對Java高并發(fā)、分布式、JVM、微服務(wù)等技術(shù)感興趣的可以進(jìn)我的Java交流群:582100479,歡迎大家轉(zhuǎn)發(fā)討論!

盡管CMS是大內(nèi)存的首選,但是CMS仍然是有一些令人不滿意的地方,比如搶占CPU資源、內(nèi)存碎片等問題。不過總而言之,CMS目前在大內(nèi)存的使用上,仍然是首選。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容