不會(huì)JVM調(diào)優(yōu)怎么進(jìn)互聯(lián)網(wǎng)大廠
如果說有什么在面試中經(jīng)常被問到,但是在實(shí)際工作中又不經(jīng)常用到的Java技術(shù),那么JVM調(diào)優(yōu)絕對(duì)可以排得上號(hào)。每當(dāng)有同學(xué)被問到這個(gè)問題的時(shí)候,內(nèi)心的OS大概是這樣:我一個(gè)QPS幾百的系統(tǒng),有啥好調(diào)優(yōu)的,默認(rèn)配置用用得了,調(diào)JVM參數(shù)整不好系統(tǒng)還能干崩了,想想好像是這么個(gè)道理。但是對(duì)于一些高并發(fā)大流量業(yè)務(wù)場(chǎng)景,JVM調(diào)優(yōu)就有用武之地了。因此個(gè)人覺得JVM調(diào)優(yōu)也許不像SQL調(diào)優(yōu)或者代碼優(yōu)化在工作中使用得那么頻繁,甚至很多時(shí)候其實(shí)是用不上的,但是在需要用到的時(shí)候如果你能頂?shù)米毫ν瓿蓛?yōu)化,那么你和其他人的不同就顯現(xiàn)出來了。另外如果我們想進(jìn)入一線互聯(lián)網(wǎng)大廠,那么JVM調(diào)優(yōu)就是必須要掌握的重要技能。
那么JVM到底應(yīng)該怎樣調(diào)優(yōu)呢?有沒有什么套路我們可以學(xué)習(xí)?本文主要著眼于如何進(jìn)行參數(shù)預(yù)估以及JVM優(yōu)化,希望對(duì)大家平時(shí)工作可以有所裨益。
預(yù)估比調(diào)優(yōu)更重要
為什么需要進(jìn)行預(yù)估
所謂凡事預(yù)則立不預(yù)則廢,對(duì)于JVM調(diào)優(yōu)來說也是如此。無論修改線上已有JVM參數(shù)配置還是優(yōu)化代碼實(shí)際都是一種無奈之舉,因?yàn)樯a(chǎn)環(huán)境出現(xiàn)了運(yùn)行異常不得不采用這種方式進(jìn)行優(yōu)化,從而保障線上應(yīng)用服務(wù)能夠正常運(yùn)行,否則就要拉程序員出來祭天了。但是如果我們?cè)诜?wù)發(fā)布部署之前可以預(yù)估服務(wù)的容量而后進(jìn)行對(duì)應(yīng)的JVM參數(shù)配置,那么就相當(dāng)于把可能出現(xiàn)的JVM異常扼殺在搖籃中。當(dāng)然這是最理想的狀態(tài),在現(xiàn)實(shí)中也實(shí)際不容易做到,但是即便我們不能預(yù)估的那么準(zhǔn)確,也總比不做容量預(yù)估直接裸奔上線的好。因此關(guān)于JVM調(diào)優(yōu)這件事情,實(shí)際和《孫子兵法》的核心思想有異曲同工之妙,上兵伐謀,其次伐交,其次伐兵,其下攻城。也就是說JVM調(diào)優(yōu)最高境界是預(yù)估不調(diào),打仗最高境界是不戰(zhàn)而勝。
如何進(jìn)行預(yù)估
JVM參數(shù)預(yù)估基本流程
在明確了JVM參數(shù)預(yù)估對(duì)于生產(chǎn)環(huán)境中服務(wù)穩(wěn)定運(yùn)行的意義之后,我們一起看下如何進(jìn)行JVM參數(shù)預(yù)估。首先我們應(yīng)該先分析下自己系統(tǒng)的核心業(yè)務(wù)流程是什么,然后根據(jù)核心業(yè)務(wù)流程結(jié)合線上可能的流量,確認(rèn)好我們核心業(yè)務(wù)代碼中的對(duì)象創(chuàng)建以及銷毀情況是怎樣的,最后再針對(duì)性的進(jìn)行相關(guān)JVM參數(shù)的預(yù)估配置。因此JVM參數(shù)預(yù)估的地址基本流程如下所示:
案例驅(qū)動(dòng)
這里以一個(gè)實(shí)際的業(yè)務(wù)場(chǎng)景案例來幫助大家更好理解JVM參數(shù)預(yù)估的過程。假設(shè)有這樣一個(gè)電商平臺(tái),它主要由商品中心、訂單中心、營(yíng)銷中心、庫(kù)存中心等子系統(tǒng)組成。那對(duì)于電商系統(tǒng)來說最核心的業(yè)務(wù)就是用戶下單購(gòu)物,我們就以用戶下單購(gòu)物這個(gè)業(yè)務(wù)流程來看看如何進(jìn)行估算JVM參數(shù)。
假設(shè)平臺(tái)有1個(gè)億的注冊(cè)用戶,日活用戶1000萬,這些用戶會(huì)在電商平臺(tái)進(jìn)行瀏覽商品、下單購(gòu)買以及收貨評(píng)價(jià)等操作,但是實(shí)際上真正下單購(gòu)買的用戶并沒有那么多,如果有10%的轉(zhuǎn)化率,那么就相當(dāng)于每天電商平臺(tái)有100w個(gè)訂單。另外一般情況下這些訂單主要分布在一天當(dāng)中的高峰時(shí)間,比如中午或者晚上,畢竟其他時(shí)間大家要忙碌工作以及其他事情,中午休息或者晚上休息的時(shí)候才會(huì)有時(shí)間逛平臺(tái)買買買。如果我們把用戶購(gòu)買的高峰時(shí)間定為3小時(shí),也就是說極端情況下將所有的訂單的生成都分布在這三個(gè)小時(shí)中完成,也就是每小時(shí)產(chǎn)生33萬個(gè)訂單,每秒產(chǎn)生92個(gè)訂單左右。
在估算訂單對(duì)象大小之前,我們先來看下堆中的對(duì)象由哪些元素組成。一個(gè)JVM對(duì)象的大小主要由三部分組成,分別是對(duì)象頭、數(shù)據(jù)以及數(shù)據(jù)補(bǔ)齊。對(duì)象頭以及對(duì)象補(bǔ)齊基本變化不大,因此對(duì)象的大小實(shí)際和對(duì)象中的屬性有直接關(guān)系,對(duì)象中的屬性越多,對(duì)象占用的空間大小也就越大。
Mark Word:主要存儲(chǔ)對(duì)象自身的運(yùn)行數(shù)據(jù),包括HashCode、GC分代年齡鎖狀態(tài)標(biāo)志、線程持有的鎖等信息,根據(jù)操作系統(tǒng)位數(shù)的不同而不同,32位的操作系統(tǒng)對(duì)應(yīng)的大小就是32bit,64位的操作系統(tǒng)對(duì)應(yīng)的大小就是64bit;
Klass Pointer:指向?qū)ο髮?duì)應(yīng)的Class對(duì)象的內(nèi)存地址,根據(jù)不同的操作數(shù)系統(tǒng)占用空間不同,在64位系統(tǒng)中占用8個(gè)字節(jié);
Array Length:如果當(dāng)前對(duì)象是一個(gè)數(shù)組對(duì)象那么此處存儲(chǔ)的就是數(shù)組的大小,占用4個(gè)字節(jié),如果不是數(shù)組對(duì)象那么就不占用。
回到我們剛才的案例當(dāng)中,我們來具體估算一個(gè)訂單對(duì)象大概占多少內(nèi)存空間。訂單對(duì)象主要包括了如下的屬性:訂單編號(hào)、商品編號(hào)、商品價(jià)格、創(chuàng)建時(shí)間、付款時(shí)間以及發(fā)貨時(shí)間等,當(dāng)然實(shí)際訂單可能不止這些屬性,我們只是說明對(duì)象大小估算的方法,因此對(duì)屬性進(jìn)行了相應(yīng)的裁剪。如果數(shù)據(jù)層面包含了這些屬性,那么數(shù)據(jù)部分的占用空間大小就是這些屬性的大小總和??偣补浪阆聛響?yīng)該不到1kb,但是實(shí)際考慮到其他各種占用以及平臺(tái)中肯定不止訂單這一種對(duì)象,還會(huì)有庫(kù)存對(duì)象、積分對(duì)象、物流對(duì)象、營(yíng)銷對(duì)象等等,因此我們考慮將對(duì)象的總和擴(kuò)大30倍進(jìn)行估算,也就是說平臺(tái)中產(chǎn)生的各種對(duì)象的總和為30乘以1kb即為30kb。如果每秒產(chǎn)生92個(gè)對(duì)象的話,那么就相當(dāng)于每秒產(chǎn)生2760kb的對(duì)象,也就是大概2Mb的對(duì)象,另外由于電商平臺(tái)中布置下單這一個(gè)操作,還會(huì)包含訂單查詢、商品查詢等等其他業(yè)務(wù)那么綜合起來我們?cè)俜糯?0倍,也就是說每秒JVM中新增20Mb左右的對(duì)象。對(duì)于一臺(tái)4核8G的服務(wù)器來說,我們可以為服務(wù)分配3G左右的堆內(nèi)存,512Mb左右的元空間。
但是考慮到電商平臺(tái)存在大促場(chǎng)景,這個(gè)時(shí)候的流量可能是平時(shí)的好幾倍,因此我們實(shí)際上需要將堆內(nèi)存中的年輕代進(jìn)行放大,Eden區(qū)可以到1.6G,Survivor區(qū)可以各自200M。這樣可以避免由于年輕代空間不足導(dǎo)致對(duì)象提前進(jìn)入老年代而造成fullGC的頻率變高,從而影響服務(wù)的穩(wěn)定性。
JVM調(diào)優(yōu)思路JVM
理想情況下預(yù)估的JVM參數(shù)應(yīng)該可以cover線上的業(yè)務(wù)場(chǎng)景,但是假如公司業(yè)務(wù)發(fā)展飛快,業(yè)務(wù)體量迅速膨脹,原先預(yù)估的JVM配置參數(shù)可就不一定能滿足線上生產(chǎn)環(huán)境所有情況,因此異常情況還是會(huì)出現(xiàn)。這里將JVM異常主要分為兩類,一種是代碼導(dǎo)致的JVM異常,另一種是JVM不合理配置導(dǎo)致的異常,包括JVM參數(shù)以及服務(wù)器內(nèi)存配置。
代碼導(dǎo)致的JVM異常
代碼Bug應(yīng)該是導(dǎo)致JVM異常最常見的情況,這種情況我們只能通過調(diào)整代碼才能實(shí)現(xiàn)優(yōu)化,因?yàn)榧词古R時(shí)調(diào)整JVM參數(shù)也只是緩兵之計(jì),并沒有根除問題所在,隨著時(shí)間的推移,業(yè)務(wù)的發(fā)展問題還是會(huì)暴露。所以要想解決根本問題還是需要定位問題代碼來進(jìn)行優(yōu)化。
那么首先我們就需要能夠有手段定位到到底哪部分代碼導(dǎo)致JVM異常。一般分為兩種情況,一種就是已經(jīng)發(fā)生內(nèi)存溢出了,另一種是還沒有發(fā)生內(nèi)存溢出但是已經(jīng)在崩潰的邊緣,系統(tǒng)響應(yīng)也變慢了。如果我們配置了-XX:HeapDumpPath參數(shù),當(dāng)JVM發(fā)生內(nèi)存溢出的時(shí)候就可以到對(duì)應(yīng)的目錄去找到hprof文件。如果還沒有發(fā)生內(nèi)存溢出,這個(gè)時(shí)候我們可以通過操作命令jmap -dump:format=b,file=/tmp/文件名.hprof <PID>來手動(dòng)導(dǎo)出內(nèi)存快照來進(jìn)行進(jìn)行分析。有了hprof文件之后,我們可以通過MAT工具來分析和定位內(nèi)存溢出代碼位置,然后再進(jìn)行針對(duì)性的優(yōu)化。
Java代碼引起的JVM異??梢苑譃橐韵聨追N情況,我們一起來看看有哪些:
(1)如下圖的例子,客戶端和服務(wù)端建立了websocket連接,如果連接未正常建立,又重新建立連接如果此時(shí)服務(wù)端未將連接關(guān)閉,那么就會(huì)導(dǎo)致重新使用新的請(qǐng)求對(duì)象,隨著時(shí)間的累計(jì)JVM中出現(xiàn)大量對(duì)象來不及回收,導(dǎo)致JVM無法分配新的內(nèi)存空間給服務(wù)中新產(chǎn)生的對(duì)象,最終導(dǎo)致JVM內(nèi)存溢出。由于JVM中堆積了幾千個(gè)RequestIInfo對(duì)象,同時(shí)服務(wù)還在不斷產(chǎn)生新的RequestInfo對(duì)象,最終不可避免地就會(huì)發(fā)生OutOfMemoryError異常。通過MAT我們可以輕松定位到發(fā)生內(nèi)存溢出的代碼位置,搞清楚為什么會(huì)有RequestInfo對(duì)象被創(chuàng)建之后,我們就可以進(jìn)行針對(duì)性的優(yōu)化了。
(2)我們?cè)趯?shí)際項(xiàng)目開發(fā)的過程中必定會(huì)涉及到業(yè)務(wù)數(shù)據(jù)的查詢,假如沒有控制好數(shù)據(jù)查詢的條件或者說本身查詢的數(shù)據(jù)量就很大。那么就容易造成一次性查詢大量數(shù)據(jù),這些數(shù)據(jù)如果全部load到內(nèi)存中就很容易導(dǎo)致內(nèi)存溢出。所以一般涉及到數(shù)據(jù)查詢的代碼要做好相應(yīng)的處理,分頁(yè)查詢也好,限制查詢數(shù)據(jù)量也好或者流式查詢也好,總之不能一次性將大量數(shù)據(jù)加載到內(nèi)存中。
(3)在for循環(huán)或者while循環(huán)中大量創(chuàng)建對(duì)象,最終導(dǎo)致對(duì)象在對(duì)堆內(nèi)存中堆積,這種是由于條件沒有控制好條件導(dǎo)致對(duì)象被不斷創(chuàng)建。
(4)我們都知道JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的虛擬機(jī)棧是線程所獨(dú)有的,JVM啟動(dòng)后會(huì)為每個(gè)線程的虛擬機(jī)棧分配固定大小的內(nèi)存(-Xss參數(shù)),因此虛擬機(jī)棧的深度是確定的,如果代碼中出現(xiàn)不合理的遞歸代碼,就會(huì)造成虛擬機(jī)棧只入棧不出棧,最終導(dǎo)致虛擬機(jī)棧內(nèi)存空間被耗盡,從而產(chǎn)生StackOverFlowError。
當(dāng)我們知道了這些常見的可能導(dǎo)致JVM異常的代碼結(jié)構(gòu)之后,那么在平常做項(xiàng)目編寫代碼的時(shí)候就要時(shí)刻保持警惕。寫完代碼之后自己再回頭看看這段代碼的對(duì)象創(chuàng)建情況是怎樣的,有沒有大對(duì)象緩存,有沒有不合理的while循環(huán)for循環(huán),會(huì)不會(huì)有可能造成JVM內(nèi)存溢出。當(dāng)我們有了這樣反觀代碼的意識(shí)之后,從根本上JVM內(nèi)存溢出的概率大大降低,有益于線上服務(wù)的穩(wěn)定性。
JVM參數(shù)不合理導(dǎo)致的異常
線上環(huán)境JVM參數(shù)不合理直接影響JVM運(yùn)行穩(wěn)定性。我們都知道對(duì)象都是存放在堆內(nèi)存中的,而堆內(nèi)存又被劃分為年輕代和老年代,新產(chǎn)生的對(duì)象都會(huì)被分配在年輕代對(duì)應(yīng)的堆內(nèi)存中,如果此時(shí)我們?cè)O(shè)置的年輕代過小。那么對(duì)象進(jìn)入到老年代堆空間的概率就會(huì)增大,當(dāng)然引起full GC的可能性也會(huì)大大增加。因此JVM參數(shù)如果設(shè)置的不合理一般是堆內(nèi)存大小、元數(shù)據(jù)區(qū)大小以及垃圾回收器。另外我們需要根據(jù)不同的業(yè)務(wù)場(chǎng)景來選擇對(duì)應(yīng)的垃圾回收器,如果對(duì)于停頓時(shí)間有比較高的要求可以考慮G1和ZGC。
通過上文我們可以明確無論是優(yōu)化業(yè)務(wù)代碼還是參數(shù)調(diào)優(yōu),其實(shí)都是在避免在堆中遺留過多的對(duì)象??梢钥吹贸鰜恚琂VM調(diào)優(yōu)的本質(zhì)思想其實(shí)就是生產(chǎn)者-消費(fèi)者模型,為什么這么說呢?你看一方面隨著平臺(tái)業(yè)務(wù)的不斷進(jìn)行,JVM中會(huì)不斷產(chǎn)生對(duì)象,那么平臺(tái)就相當(dāng)于對(duì)象生產(chǎn)者。另一方面垃圾回收器這個(gè)勤勞的小蜜蜂在不斷檢測(cè)哪些對(duì)象已經(jīng)是垃圾對(duì)象,然后根據(jù)策略進(jìn)行垃圾回收釋放內(nèi)存空間,那么JVM就相當(dāng)于對(duì)象消費(fèi)者。一個(gè)生產(chǎn)對(duì)象,一個(gè)消費(fèi)對(duì)象,這可不就是生產(chǎn)者消費(fèi)者模型嘛。所以從這個(gè)角度來看,生產(chǎn)者和消費(fèi)者的動(dòng)態(tài)平衡才能保證JVM的正常運(yùn)行,如果對(duì)象生產(chǎn)地快,而對(duì)象回收地慢就會(huì)導(dǎo)致內(nèi)存溢出等JVM異常。所以JVM調(diào)優(yōu)從本質(zhì)上來說,就是通過各種手段構(gòu)建對(duì)象生產(chǎn)與回收的動(dòng)態(tài)平衡。
JVM常見配置參數(shù)
無論是發(fā)布部署前的JVM參數(shù)預(yù)估還是異常過后的參數(shù)優(yōu)化,都是需要通過調(diào)節(jié)JVM對(duì)應(yīng)的參數(shù)來完成的,因此我們需要掌握常用JVM參數(shù)項(xiàng)及其含義。
配置項(xiàng) | 含義 |
-Xms | 初始堆大小 |
-Xmx | 初始堆最大值 |
-Xmn | 堆中新生代最大值 |
-XX:SurvivorRatio | survivor區(qū)與Eden區(qū)的比例 |
-XX:NewRatio | 新生代和老年代的比例 |
-XX:MetaspaceSize | 初始元空間大小 |
-XX:MaxMetaspaceSize | 元空間最大大小 |
-Xss | 線程虛擬機(jī)棧大小 |
-XX:+HeapDumpOnOutOfMemoryError | 開啟內(nèi)存溢出時(shí)進(jìn)行內(nèi)存快照 |
-XX:HeapDumpPath=/data/dump/jvm.hprof | 內(nèi)存快照文件路徑 |
JVM常見垃圾回收器
隨著JDK版本的不斷迭代,垃圾回收器同樣也在不斷迭代優(yōu)化。當(dāng)然不同的業(yè)務(wù)場(chǎng)景,我們可以選擇不同的垃圾回收器來進(jìn)行應(yīng)對(duì),而垃圾回收器也在向著回收效率高、停頓時(shí)間短的目標(biāo)不斷進(jìn)行調(diào)整改進(jìn)。
垃圾回收器 | 引入版本 | 特點(diǎn) | 適用場(chǎng)景 |
Serial GC | JDK3 | 單線程方式進(jìn)行垃圾回收,暫停所有應(yīng)用線程 | 適用于小型應(yīng)用,單CPU的系統(tǒng)或者不需要高并發(fā)的場(chǎng)景 |
ParNew GC | JDK3 | Serial GC多線程實(shí)現(xiàn) | 年輕代垃圾回收 |
Parallel GC | JDK4 | 利用多CPU、多核心的系統(tǒng)資源,提高垃圾回收效率 | 對(duì)吞吐量有要求的應(yīng)用場(chǎng)景,如數(shù)據(jù)處理、科學(xué)計(jì)算等 |
CMS GC | JDK5 | 采用多線程方式進(jìn)行垃圾回收,能夠縮短應(yīng)用程序的暫停時(shí)間 | 適用于對(duì)響應(yīng)時(shí)間有要求的應(yīng)用場(chǎng)景 |
G1 GC | JDK7 | 內(nèi)存劃分為一個(gè)個(gè)Region,可以指定停頓時(shí)間 | 適用于部署早多核CPU大內(nèi)存機(jī)器上的大型應(yīng)用,對(duì)停頓時(shí)間有一定要求, |
ZGC | JDK11 | 支持超大堆空間,最大停頓時(shí)間不超過10ms | 業(yè)務(wù)對(duì)于停頓時(shí)間低于100ms |
總結(jié)
任何技術(shù)上的優(yōu)化都是建立在對(duì)技術(shù)原理的深刻理解基礎(chǔ)之上的,JVM調(diào)優(yōu)亦是如此。文章中常見的JVM調(diào)優(yōu)手段只不過是一些術(shù),搞懂JVM的運(yùn)行原理以及垃圾回收機(jī)制才是關(guān)鍵。另外在調(diào)優(yōu)前我們得先搞清楚我們調(diào)優(yōu)的目標(biāo)是什么,有了目標(biāo)的指引,我們才能做到有的放矢。其實(shí)無論是性能優(yōu)化還是業(yè)務(wù)優(yōu)化其實(shí)都是有一定的規(guī)律可以摸索,萬變不離其宗,都是通過觀:觀察當(dāng)前是個(gè)什么樣的狀態(tài);析:分析整條業(yè)務(wù)鏈路找到可以優(yōu)化的方向以及改造點(diǎn);優(yōu):動(dòng)手制定優(yōu)化策略以及驗(yàn)證方法進(jìn)行優(yōu)化實(shí)操。