單核CPU, 1G內(nèi)存,也能做JVM調(diào)優(yōu)嗎?
最近,筆者的技術(shù)群里有人問了一個有趣的技術(shù)話題:單核CPU, 1G內(nèi)存的超低配機器,怎么做JVM調(diào)優(yōu)?
這實際上是兩個問題。單核CPU的超低配機器,怎么充分利用CPU?單核CPU, 1G內(nèi)存的超低配機器,怎么做JVM調(diào)優(yōu)?
怎么充分利用CPU?
這個問題不能一概而論,要結(jié)合具體場景。對于IO密集型和CPU密集型的應(yīng)用調(diào)優(yōu)的方法會截然不同。
IO密集型:有頻繁外部設(shè)備訪問的應(yīng)用,如磁盤訪問和網(wǎng)絡(luò)訪問等。由于CPU性能相對硬盤讀寫和網(wǎng)絡(luò)訪問要好很多,系統(tǒng)執(zhí)行任務(wù)時,大部分的情況是CPU在等I/O (磁盤/網(wǎng)絡(luò)) 的讀/寫操作,在發(fā)生I/O操作時cpu處于等待狀態(tài),這就可能導(dǎo)致cpu的利用率不高。
CPU密集型: 以計算為主,很少有磁盤和網(wǎng)絡(luò)訪問的應(yīng)用。這種任務(wù)CPU一直在運行,CPU的利用率很高。
在給出CPU調(diào)優(yōu)結(jié)論之前,先花兩分鐘熟悉一下I/O基礎(chǔ)。
所謂的I/O(Input/Output)操作實際上就是輸入輸出的數(shù)據(jù)傳輸行為。程序員最關(guān)注的主要是磁盤IO和網(wǎng)絡(luò)IO,因為這兩個IO操作和應(yīng)用程序的關(guān)系最直接最緊密。
磁盤IO:磁盤的輸入輸出,比如磁盤和內(nèi)存之間的數(shù)據(jù)傳輸。
網(wǎng)絡(luò)IO:不同系統(tǒng)間跨網(wǎng)絡(luò)的數(shù)據(jù)傳輸,比如兩個系統(tǒng)間的遠程接口調(diào)用。
下面這張圖展示了應(yīng)用程序中發(fā)生IO的具體場景:
通過上圖,我們可以了解到IO操作發(fā)生的具體場景。一個請求過程可能會發(fā)生很多次的IO操作:
1,頁面請求到服務(wù)器會發(fā)生網(wǎng)絡(luò)IO
2,服務(wù)之間遠程調(diào)用會發(fā)生網(wǎng)絡(luò)IO
3,應(yīng)用程序訪問數(shù)據(jù)庫會發(fā)生網(wǎng)絡(luò)IO
4,數(shù)據(jù)庫查詢或者寫入數(shù)據(jù)會發(fā)生磁盤IO
下面是執(zhí)行top命令查看CPU狀況的截圖:
從上圖,我們可以看到:
CPU空閑率是0%(上圖中紅框id)
CPU使用率是22%(上圖中紅框 us 13% 加上 sy 9%,us可以理解成用戶進程占用的CPU,sy可以理解成系統(tǒng)進程占用的CPU)
CPU 在等待磁盤IO操作上花費的時間占比是76.6% (上圖中紅框 wa)
不少人會這樣理解,如果CPU空閑率是0%,就代表CPU已經(jīng)在滿負(fù)荷工作,沒精力再處理其他任務(wù)了。真是這樣的嗎?
我們先看一下計算機是怎么管理磁盤IO操作的。計算機發(fā)展早期,磁盤和內(nèi)存的數(shù)據(jù)傳輸是由CPU控制的,也就是說從磁盤讀取數(shù)據(jù)到內(nèi)存中,是需要CPU存儲和轉(zhuǎn)發(fā)的,期間CPU一直會被占用。我們知道磁盤的讀寫速度遠遠比不上CPU的運轉(zhuǎn)速度。這樣在傳輸數(shù)據(jù)時就會占用大量CPU資源,造成CPU資源嚴(yán)重浪費。
后來有人設(shè)計了一個IO控制器,專門控制磁盤IO。當(dāng)發(fā)生磁盤和內(nèi)存間的數(shù)據(jù)傳輸前,CPU會給IO控制器發(fā)送指令,讓IO控制器負(fù)責(zé)數(shù)據(jù)傳輸操作,數(shù)據(jù)傳輸完IO控制器再通知CPU。因此,從磁盤讀取數(shù)據(jù)到內(nèi)存的過程就不再需要CPU參與了,CPU可以空出來處理其他事情,大大提高了CPU利用率。這個IO控制器就是“DMA”,即直接內(nèi)存訪問,Direct Memory Access?,F(xiàn)在的計算機基本都采用這種DMA模式進行數(shù)據(jù)傳輸。
通過上面內(nèi)容我們了解到,IO數(shù)據(jù)傳輸時,是不占用CPU的。當(dāng)應(yīng)用進程或線程發(fā)生IO等待時,CPU會及時釋放相應(yīng)的時間片資源并把時間片分配給其他進程或線程使用,從而使CPU資源得到充分利用。所以,假如CPU大部分消耗在IO等待(wa)上時,即便CPU空閑率(id)是0%,也并不意味著CPU資源完全耗盡了,如果有新的任務(wù)來了,CPU仍然有精力執(zhí)行任務(wù)。如下圖:
在DMA模式下執(zhí)行IO操作是不占用CPU的,所以CPU IO等待(上圖的wa)實際上屬于CPU空閑率的一部分。所以我們執(zhí)行top命令時,除了要關(guān)注CPU空閑率,CPU使用率(us,sy),還要關(guān)注IO Wait(wa)。注意,wa只代表磁盤IO Wait,不包括網(wǎng)絡(luò)IO Wait。
了解完IO的基礎(chǔ)知識,我們看看在單核CPU的超低配機器上,怎么充分利用CPU?
對于IO密集型應(yīng)用。CPU會有很多時間花在IO等待上,發(fā)生IO時雖然CPU空閑率(上圖的id)受到影響,但是實際上cpu并沒有干活。這時就需要較多的線程數(shù)量,當(dāng)一部分線程因為IO問題被阻塞時,其他空閑線程還能繼續(xù)接收并執(zhí)行其他請求任務(wù)。這樣cpu利用率就會更高。同時還要考慮線程間上下文切換帶來的性能開銷,線程數(shù)量不能太高。對于單核CPU,要根據(jù)IO的密集程度設(shè)置線程數(shù)。由于CPU只有一核,資源有限,所以除了對線程數(shù)的優(yōu)化外,主要還是要優(yōu)化IO操作,減少IO操作頻率,縮短IO操作時間。IO操作優(yōu)化之后,線程數(shù)可以設(shè)置成更少,線程切的換頻率和性能開銷也會隨之降低。
對于CPU密集型應(yīng)用。線程數(shù)應(yīng)該盡可能少一些,在沒有任何IO操作的情況下,為了減少線程切換帶來的性能開銷,理論上最佳的線程數(shù)量應(yīng)該設(shè)置成CPU的核數(shù)。不過實際場景中,絕大多數(shù)應(yīng)用或多或少都會有一定的IO操作(比如記錄Log,訪問數(shù)據(jù)庫或者跨網(wǎng)絡(luò)的遠程調(diào)用等),這樣線程數(shù)就需要適當(dāng)調(diào)大。至于設(shè)置成多少,就沒有定論了,需要我們多次調(diào)整驗證(取性能測試的最優(yōu)結(jié)果)。對于單核CPU,為了減少線程切換帶來的性能開銷,一兩個線程基本就夠了。
怎么做JVM調(diào)優(yōu)?
選擇合適的垃圾收集器
以CMS回收過程為例,在耗時較長的并發(fā)標(biāo)記和并發(fā)清除階段,垃圾收集線程和用戶線程是同時并行工作的,也就是說并發(fā)階段不會導(dǎo)致用戶線程停頓。不過CMS對CPU資源非常敏感。 其實,所有高并發(fā)的應(yīng)用對CPU資源都很敏感。在CMS并發(fā)階段(并發(fā)標(biāo)記和并發(fā)清除階段),雖然不會導(dǎo)致用戶線程停頓,但是垃圾收集線程會占用一部分CPU資源,進而導(dǎo)致應(yīng)用程序變慢,吞吐量降低。CMS默認(rèn)啟動的垃圾收集線程數(shù)是(CPU核數(shù)+3)/4,當(dāng)CPU核數(shù)在4個以上時,并發(fā)回收階段垃圾收集線程不少于25%的CPU資源(CPU核數(shù))。但是當(dāng)CPU核數(shù)不足4個時,比如CPU核數(shù)為2個,CMS對用戶程序的影響就可能變得很大,此時需要分配1個核的資源去執(zhí)行垃圾收集任務(wù),如果本來CPU負(fù)載就比較大,還要分出一半的計算能力去執(zhí)行垃圾收集任務(wù),就可能導(dǎo)致應(yīng)用程序的執(zhí)行速度大幅下降,甚至忽然降低50%以上,著實讓人無法接受。
在單核CPU環(huán)境下,并發(fā)標(biāo)記和并發(fā)清除階段是無法真正做到并發(fā)的,當(dāng)垃圾收集線程執(zhí)行標(biāo)記和清除任務(wù)時,單核CPU唯一的核就無法執(zhí)行用戶線程,這樣就會造成嚴(yán)重的用戶線程阻塞問題,導(dǎo)致應(yīng)用程序響應(yīng)超慢。
說到這有人可能會問:換成其他垃圾收集器,在單核CPU環(huán)境下,不一樣會有這種因為線程阻塞導(dǎo)致的應(yīng)用程序執(zhí)行變慢的問題嗎?
沒錯,換成其他垃圾收集器,在單核CPU環(huán)境下,一樣會有同樣的問題。不過情況應(yīng)該會比使用CMS或者G1要好!CMS是響應(yīng)速度優(yōu)先的老年代垃圾收集器,是一種以降低GC全局停頓時間(Stop The World)為目標(biāo)的收集器。為了實現(xiàn)這一目標(biāo),CMS把垃圾回收分成了初始標(biāo)記,并發(fā)標(biāo)記,重新標(biāo)記和并發(fā)清除4個階段。其中初始標(biāo)記和重新標(biāo)記兩個階段會停止所有用戶線程(發(fā)生STW),不過耗時很短。并發(fā)標(biāo)記和并發(fā)清除兩個階段耗時最長,但是這兩個階段垃圾收集線程可以和用戶線程一起工作,不會停止用戶線程。CMS的這種設(shè)計雖然縮短了STW的時間,但是整個GC過程(四個階段加在一起的總時間)更長了。如果在單核CPU環(huán)境下,并發(fā)標(biāo)記和并發(fā)清除兩個階段就無法做到真正的并發(fā),因為單核的問題,垃圾收集線程和用戶線程不可能同時占用唯一的CPU資源,所以在垃圾收集線程運行時所有用戶線程都會被停止,相當(dāng)于發(fā)生了STW。基本上可以這樣理解,在單核CPU環(huán)境下,CMS的四個階段都會發(fā)生Stop The World。也就是說,在單核CPU環(huán)境下,CMS的Stop The World時間比傳統(tǒng)的老年代收集器Serial Old和Parallel Old還要長。所以在單核CPU環(huán)境下,絕對不能選擇CMS和G1這種對CPU特別敏感的收集器。考慮到Parallel Old是一款多線程并發(fā)收集器,主要為了利用多核CPU來提高垃圾回收效率,不適合單核環(huán)境。所以,基本上最古老的Serial Old收集器就成了單核CPU的最佳選擇啦。
另外,1G的內(nèi)存空間太小,也不適合CMS和G1。數(shù)年前,在CMS和G1還沒誕生之前,很多互聯(lián)網(wǎng)系統(tǒng)使用Serial Old和Parallel Old做為老年代收集器,這樣會帶來一個嚴(yán)重問題,堆內(nèi)存越大垃圾回收時STW(Stop The World)時間就越長,在互聯(lián)網(wǎng)系統(tǒng)中,堆內(nèi)存往往會超過4G,每次Full GC時STW時間會很長,可能會達到幾秒鐘甚至更長,也就是說JVM在這幾秒鐘內(nèi)無法處理任何用戶請求。這在高并發(fā)的互聯(lián)網(wǎng)系統(tǒng)中是無法接受的。后來隨著CMS和G1先后應(yīng)運而生,解決了較大堆內(nèi)存GC時STW時間過長的問題。所以說CMS和G1只是為了大內(nèi)存場景設(shè)計的,不適合小內(nèi)存場景,在小內(nèi)存場景下不能發(fā)揮自己的優(yōu)勢。如果內(nèi)存只有1G,單核CPU下為了提高吞吐量可以選擇Serial Old。多核CPU下,為了充分發(fā)揮多核作用提高垃圾收集效率,可以選擇多線程并發(fā)收集器Parallel Old。
降低GC頻次
在給出具體 降低GC頻次方案之前, 我們以Java官方的HotSpot JVM為例, 先了解一下堆內(nèi)存分布以及對象的分配和流轉(zhuǎn)過程。
JVM將堆內(nèi)存分為了三部分:新生代(Young Generation),老年代(Old Generation),永久代(Permanent Generation)。其中新生代又分為三部分:伊甸園區(qū)(Eden),和兩個幸存區(qū)S0和S1。
注:JDK1.8之后,Java官方的HotSpot JVM去掉了永久代,取而代之的是元數(shù)據(jù)區(qū)Metaspace。Metaspace使用的是本地內(nèi)存,而不是堆內(nèi)存,也就是說在默認(rèn)情況下Metaspace的大小只與本地內(nèi)存的大小有關(guān)。因此JDK1.8之后,就見不到j(luò)ava.lang.OutOfMemoryError: PermGen space這種由于永久代空間不足導(dǎo)致的內(nèi)存溢出的問題了。
堆內(nèi)存中對象的分配和流轉(zhuǎn)過程
新創(chuàng)建的對象會先被分配到到Eden區(qū)。JVM剛啟動時,Eden區(qū)對象數(shù)量較少,兩個Survivor區(qū)S0、S1幾乎是空的。
隨著時間的推移,Eden區(qū)的對象越來越多。當(dāng)Eden區(qū)放不下時(占用空間達到容量閾值),新生代就會發(fā)生垃圾回收,我們稱之為Minor GC或者Young GC。
發(fā)生GC時,第一步會通過可達性分析算法找到可達對象。如上圖,藍色為可達對象,其他紫色為不可達對象。第二步,被標(biāo)示的可達對象會被轉(zhuǎn)移到S0(此時S0是From Survivor),此時存活對象年齡加1,三個對象年齡都變?yōu)?。第三步,清除Eden區(qū)所有對象。
GC后各區(qū)域?qū)ο笳加们闆r,如上圖所示。
程序繼續(xù)運行,Eden區(qū)再次達到容量閾值時,會再次發(fā)生GC。這時S0(From Survivor)已經(jīng)有了對象。還是同樣的步驟,通過可達性分析算法找到可達對象,然后再將Eden和S0中的可達對象轉(zhuǎn)移到S1(To Survivor),各存活對象年齡加1。最后將Eden和S0中的所有對象清除。
GC后S0區(qū)域被清空。如上圖所示。S0和S1發(fā)生了互換,S1變成了From Survivor,S0變成了To Survivor。
注意,To Survivor區(qū)永遠都為空。這實際上是垃圾回收算法-復(fù)制算法在年輕代的實際應(yīng)用。把年輕代分為Eden,S0,S1三個區(qū)域,每次垃圾回收時把可達對象復(fù)制到S0或S1,然后再清除掉Eden和(S1或S0)中的所有對象。由于每次GC時,新生代的可達對象非常少(絕大部分對象要被回收掉),一般不會超過新生代總體空間的10%,所以搜尋可達對象以及復(fù)制對象的成本都會非常低。而且這種復(fù)制的方式還能避免產(chǎn)生堆內(nèi)存碎片,提高內(nèi)存利用率。很多年輕代垃圾收集器都采用復(fù)制算法,如ParNew。
在程序運行過程中,新生代GC會反復(fù)發(fā)生,長壽對象會在S0和S1之間反復(fù)交換,年齡也會越來越大,當(dāng)對象達到年齡上限時,會被晉升到老年代。這個年齡上限默認(rèn)是15,可以通過參數(shù)-XX:MaxTenuringThreshold設(shè)置。如下圖,有些年輕代對象年齡達到了上限15,被轉(zhuǎn)移到了老年代。
通過上面的圖文內(nèi)容,我們了解了堆內(nèi)存中對象的分配和流轉(zhuǎn)過程。那么可以基于這些知識來做一些JVM調(diào)優(yōu)的工作。
所謂降低GC頻次,主要指的是降低Major GC(老年代GC)次數(shù)。內(nèi)存只有1G,為了減少Major GC,最簡單的做法是適當(dāng)調(diào)大老年代比例,但是老年代空間總有個上限,需要在老年代和年輕代之間找一個平衡點。還可以適當(dāng)調(diào)大MaxTenuringThreshold,來提高年輕代幸存區(qū)s0和s1的交換次數(shù),進而減少對象晉升到老年代的幾率。另外調(diào)大幸存區(qū)比例,也可以減少基于動態(tài)對象年齡判定導(dǎo)致對象晉升老年代的幾率。不管是哪種優(yōu)化手段,都需要反復(fù)調(diào)整和驗證(可以做性能測試驗證調(diào)整結(jié)果)。
再補充一個基礎(chǔ)知識點。Full GC,Major GC,Minor GC之間是什么關(guān)系?
當(dāng)前絕大部分垃圾收集器都采用分代回收的策略,年輕代和老年代的GC分別獨立進行。一般情況下,老年代Major GC是由年輕代Minor GC觸發(fā)的,Minor GC會導(dǎo)致部分存活時間較長的對象晉升到老年代,在晉升過程中如果老年代使用空間達到閾值就會發(fā)生Major GC。這種由Minor GC觸發(fā)Major GC引發(fā)整個堆內(nèi)存GC的情況,我們一般稱之為Full GC。還有一些情況也會觸發(fā)Major GC,比如大對象初始化時會跨過年輕代直接分配到老年代,這種情況觸發(fā)的Major GC和Minor GC就沒半點關(guān)系了??梢酝ㄟ^-XX:PretenureSizeThreshold參數(shù)設(shè)置大對象的大小,如果參數(shù)被設(shè)置成5MB,超過5MB的大對象會直接分配到老年代。
縮短GC時間
縮短GC時間和降低GC頻次,兩者是魚和熊掌的關(guān)系,不可兼得。如上面所說,在1G內(nèi)存單核CPU的場景下,響應(yīng)時間優(yōu)先的CMS和G1都不適合。在垃圾收集器沒有太多選擇的情況下,如果想縮短Major GC時間,基本上只能減小老年代的比例了,老年代空間越小,每次Major GC需要處理的對象就越少,GC時間也就越短。老年代空間越小,GC的頻次自然也會更高,內(nèi)存空間就那么多,所以我們需要反復(fù)試驗,在GC頻次和GC時間上找到最佳平衡點來滿足業(yè)務(wù)系統(tǒng)的要求。
結(jié)語
JVM調(diào)優(yōu)沒有什么可以拿來即用的固定模板或規(guī)范,每個應(yīng)用都有自己的獨特場景。不同的應(yīng)用并發(fā)程度不一樣,對響應(yīng)時間和吞吐量要求也不一樣,堆內(nèi)存對象規(guī)模、對象生命周期、對象大小等等都不會完全一樣,這些因素都會影響到JVM的性能。所以,JVM調(diào)優(yōu)是一個循序漸進的過程,必然需要經(jīng)歷多次迭代,最終才能得到一個較好的折中方案。
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!