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