您現在的位置是:首頁 > 動漫首頁動漫

單核CPU, 1G記憶體,也能做JVM調優嗎?

由 楊建榮的資料庫筆記 發表于 動漫2022-12-09
簡介數年前,在CMS和G1還沒誕生之前,很多網際網路系統使用Serial Old和Parallel Old做為老年代收集器,這樣會帶來一個嚴重問題,堆記憶體越大垃圾回收時STW(Stop The World)時間就越長,在網際網路系統中,堆記憶

sy一半可以停止嗎

最近,筆者的技術群裡有人問了一個有趣的技術話題:單核CPU, 1G記憶體的超低配機器,怎麼做JVM調優?

這實際上是兩個問題。單核CPU的超低配機器,怎麼充分利用CPU?單核CPU, 1G記憶體的超低配機器,怎麼做JVM調優?

怎麼充分利用CPU?

這個問題不能一概而論,要結合具體場景。

對於IO密集型和CPU密集型的應用調優的方法會截然不同。

IO密集型:有頻繁外部裝置訪問的應用,如磁碟訪問和網路訪問等。由於CPU效能相對硬碟讀寫和網路訪問要好很多,系統執行任務時,大部分的情況是CPU在等I/O (磁碟/網路) 的讀/寫操作,在發生I/O操作時cpu處於等待狀態,這就可能導致cpu的利用率不高。

CPU密集型: 以計算為主,很少有磁碟和網路訪問的應用。這種任務CPU一直在執行,CPU的利用率很高。

在給出CPU調優結論之前,先花兩分鐘熟悉一下I/O基礎。

所謂的I/O(Input/Output)操作實際上就是輸入輸出的資料傳輸行為。

程式設計師最關注的主要是磁碟IO和網路IO,因為這兩個IO操作和應用程式的關係最直接最緊密。

磁碟IO:磁碟的輸入輸出,比如磁碟和記憶體之間的資料傳輸。

網路IO:不同系統間跨網路的資料傳輸,比如兩個系統間的遠端介面呼叫。

下面這張圖展示了應用程式中發生IO的具體場景:

單核CPU, 1G記憶體,也能做JVM調優嗎?

透過上圖,我們可以瞭解到IO操作發生的具體場景。一個請求過程可能會發生很多次的IO操作:

1,頁面請求到伺服器會發生網路IO

2,服務之間遠端呼叫會發生網路IO

3,應用程式訪問資料庫會發生網路IO

4,資料庫查詢或者寫入資料會發生磁碟IO

下面是執行top命令檢視CPU狀況的截圖:

單核CPU, 1G記憶體,也能做JVM調優嗎?

從上圖,我們可以看到:

CPU空閒率是0%(上圖中紅框id)

CPU使用率是22%(上圖中紅框 us 13% 加上 sy 9%,us可以理解成使用者程序佔用的CPU,sy可以理解成系統程序佔用的CPU)

CPU 在等待磁碟IO操作上花費的時間佔比是76。6% (上圖中紅框 wa)

不少人會這樣理解,如果CPU空閒率是0%,

就代表CPU已經在滿負荷工作,沒精力再處理其他任務了。

真是這樣的嗎?

我們先看一下計算機是怎麼管理磁碟IO操作的。計算機發展早期,磁碟和記憶體的資料傳輸是由CPU控制的,也就是說從磁碟讀取資料到記憶體中,是需要CPU儲存和轉發的,期間CPU一直會被佔用。我們知道磁碟的讀寫速度遠遠比不上CPU的運轉速度。這樣在傳輸資料時就會佔用大量CPU資源,造成CPU資源嚴重浪費。

後來有人設計了一個IO控制器,專門控制磁碟IO。當發生磁碟和記憶體間的資料傳輸前,CPU會給IO控制器傳送指令,讓IO控制器負責資料傳輸操作,資料傳輸完IO控制器再通知CPU。因此,從磁碟讀取資料到記憶體的過程就不再需要CPU參與了,CPU可以空出來處理其他事情,大大提高了CPU利用率。這個IO控制器就是“

DMA

”,即

直接記憶體訪問,Direct Memory Access

。現在的計算機基本都採用這種DMA模式進行資料傳輸。

單核CPU, 1G記憶體,也能做JVM調優嗎?

透過上面內容我們瞭解到,IO資料傳輸時,是不佔用CPU的。

當應用程序或執行緒發生IO等待時,CPU會及時釋放相應的時間片資源並把時間片分配給其他程序或執行緒使用,從而使CPU資源得到充分利用。所以,

假如CPU大部分消耗在IO等待(wa)上時,即便CPU空閒率(id)是0%,也並不意味著CPU資源完全耗盡了,如果有新的任務來了,CPU仍然有精力執行任務。如下圖:

在DMA模式下執行IO操作是不佔用CPU的,所以CPU IO等待(上圖的wa)實際上屬於CPU空閒率的一部分。所以我們執行top命令時,除了要關注CPU空閒率,CPU使用率(us,sy),還要關注IO Wait(wa)。注意,wa只代表磁碟IO Wait,不包括網路IO Wait。

瞭解完IO的基礎知識,我們看看在單核CPU的超低配機器上,怎麼充分利用CPU?

對於IO密集型應用。

CPU會有很多時間花在IO等待上,發生IO時雖然CPU空閒率(上圖的

id

)受到影響,但是實際上cpu並沒有幹活。這時就需要較多的執行緒數量,當一部分執行緒因為IO問題被阻塞時,其他空閒執行緒還能繼續接收並執行其他請求任務。這樣cpu利用率就會更高。

同時還要考慮執行緒間上下文切換帶來的效能開銷,執行緒數量不能太高。對於單核CPU,要根據IO的密集程度設定執行緒數。

由於CPU只有一核,資源有限,所以除了對執行緒數的最佳化外,主要還是要最佳化IO操作,減

少IO操作頻率,縮短IO操作時間。IO操作最佳化之後,執行緒數可以設定成更少,執行緒切的換頻率和效能開銷也會隨之降低。

對於CPU密集型應用。

執行緒數應該儘可能少一些,在沒有任何IO操作的情況下,為了減少執行緒切換帶來的效能開銷,理論上最佳的執行緒數量應該設定成CPU的核數。不過實際場景中,絕大多數應用或多或少都會有一定的IO操作(比如記錄Log,訪問資料庫或者跨網路的遠端呼叫等),這樣執行緒數就需要適當調大。至於設定成多少,就沒有定論了,需要我們多次調整驗證(取效能測試的最優結果)。

對於單核CPU,為了減少執行緒切換帶來的效能開銷

,一兩個執行緒基本就夠了。

怎麼做JVM調優?

選擇合適的垃圾收集器

CMS和G1是目前最炙手可熱的兩個垃圾回收器,基本上所有公司都在使用CMS或G1。不過,在單核CPU,記憶體只有1G的機器上,CMS和G1就不太合適了。

以CMS回收過程為例,在耗時較長的併發標記和併發清除階段,垃圾收集執行緒和使用者執行緒是同時並行工作的,也就是說併發階段不會導致使用者執行緒停頓。不過CMS對CPU資源非常敏感。 其實,所有高併發的應用對CPU資源都很敏感。在CMS併發階段(併發標記和併發清除階段),雖然不會導致使用者執行緒停頓,但是垃圾收集執行緒會佔用一部分CPU資源,進而導致應用程式變慢,吞吐量降低。CMS預設啟動的垃圾收集執行緒數是(CPU核數+3)/4,當CPU核數在4個以上時,併發回收階段垃圾收集執行緒不少於25%的CPU資源(CPU核數)。但是當CPU核數不足4個時,比如CPU核數為2個,CMS對使用者程式的影響就可能變得很大,此時需要分配1個核的資源去執行垃圾收集任務,如果本來CPU負載就比較大,還要分出一半的計算能力去執行垃圾收集任務,就可能導致應用程式的執行速度大幅下降,甚至忽然降低50%以上,著實讓人無法接受。

在單核CPU環境下,併發標記和併發清除階段是無法真正做到併發的,當垃圾收集執行緒執行標記和清除任務時,單核CPU唯一的核就無法執行使用者執行緒,這樣就會造成嚴重的使用者執行緒阻塞問題,導致應用程式響應超慢。

說到這有人可能會問:換成其他垃圾收集器,在單核CPU環境下,不一樣會有這種因為執行緒阻塞導致的應用程式執行變慢的問題嗎?

沒錯,換成其他垃圾收集器,在單核CPU環境下,一樣會有同樣的問題。不過情況應該會比使用CMS或者G1要好!

CMS是響應速度優先的老年代垃圾收集器,是一種以降低GC全域性停頓時間(Stop The World)為目標的收集器。

為了實現這一目標,CMS把垃圾回收分成了初始標記,併發標記,重新標記和併發清除4個階段。

其中初始標記和重新標記兩個階段會停止所有使用者執行緒(發生STW),不過耗時很短。

併發標記和併發清除兩個階段耗時最長,但是這兩個階段垃圾收集執行緒可以和使用者執行緒一起工作,不會停止使用者執行緒。

CMS的這種設計雖然縮短了STW的時間,但是整個GC過程(四個階段加在一起的總時間)更長了。

如果在單核CPU環境下,併發標記和併發清除兩個階段就無法做到真正的併發,因為單核的問題,垃圾收集執行緒和使用者執行緒不可能同時佔用唯一的CPU資源,所以在垃圾收集執行緒執行時所有使用者執行緒都會被停止,相當於發生了STW。

基本上可以這樣理解,在單核CPU環境下,CMS的四個階段都會發生Stop The World。

也就是說,在單核CPU環境下,CMS的Stop The World時間比傳統的老年代收集器Serial Old和Parallel Old還要長。

所以在單核CPU環境下,絕對不能選擇CMS和G1這種對CPU特別敏感的收集器。

考慮到Parallel Old是一款多執行緒併發收集器,主要為了利用多核CPU來提高垃圾回收效率,不適合單核環境。

所以,基本上最古老的Serial Old收集器就成了單核CPU的最佳選擇啦。

另外,1G的記憶體空間太小,也不適合CMS和G1。數年前,在CMS和G1還沒誕生之前,很多網際網路系統使用Serial Old和Parallel Old做為老年代收集器,這樣會帶來一個嚴重問題,堆記憶體越大垃圾回收時STW(Stop The World)時間就越長,在網際網路系統中,堆記憶體往往會超過4G,每次Full GC時STW時間會很長,可能會達到幾秒鐘甚至更長,也就是說JVM在這幾秒鐘內無法處理任何使用者請求。這在高併發的網際網路系統中是無法接受的。後來隨著CMS和G1先後應運而生,解決了較大堆記憶體GC時STW時間過長的問題。所以說CMS和G1只是為了大記憶體場景設計的,不適合小記憶體場景,在小記憶體場景下不能發揮自己的優勢。如果記憶體只有1G,單核CPU下為了提高吞吐量可以選擇Serial Old。多核CPU下,為了充分發揮多核作用提高垃圾收集效率,可以選擇多執行緒併發收集器Parallel Old。

降低GC頻次

在給出具體

降低GC頻次

方案之前,

我們以Java官方的HotSpot JVM為例

先了解一下堆記憶體分佈以及物件的分配和流轉過程

單核CPU, 1G記憶體,也能做JVM調優嗎?

JVM將堆記憶體分為了三部分:新生代(Young Generation),老年代(Old Generation),永久代(Permanent Generation)。其中新生代又分為三部分:伊甸園區(Eden),和兩個倖存區S0和S1。

注:JDK1。8之後,Java官方的HotSpot JVM去掉了永久代,取而代之的是元資料區Metaspace。Metaspace使用的是本地記憶體,而不是堆記憶體,也就是說在預設情況下Metaspace的大小隻與本地記憶體的大小有關。因此JDK1。8之後,就見不到java。lang。OutOfMemoryError: PermGen space這種由於永久代空間不足導致的記憶體溢位的問題了。

降低GC頻次

單核CPU, 1G記憶體,也能做JVM調優嗎?

新建立的物件會先被分配到到Eden區。JVM剛啟動時,Eden區物件數量較少,兩個Survivor區S0、S1幾乎是空的。

單核CPU, 1G記憶體,也能做JVM調優嗎?

隨著時間的推移,Eden區的物件越來越多。當Eden區放不下時(佔用空間達到容量閾值),新生代就會發生垃圾回收,我們稱之為Minor GC或者Young GC。

單核CPU, 1G記憶體,也能做JVM調優嗎?

發生GC時,第一步會透過可達性分析演算法找到可達物件。如上圖,藍色為可達物件,其他紫色為不可達物件。第二步,被標示的可達物件會被轉移到S0(此時S0是From Survivor),此時存活物件年齡加1,三個物件年齡都變為1。第三步,清除Eden區所有物件。

單核CPU, 1G記憶體,也能做JVM調優嗎?

GC後各區域物件佔用情況,如上圖所示。

單核CPU, 1G記憶體,也能做JVM調優嗎?

程式繼續執行,Eden區再次達到容量閾值時,會再次發生GC。這時S0(From Survivor)已經有了物件。還是同樣的步驟,透過可達性分析演算法找到可達物件,然後再將Eden和S0中的可達物件轉移到S1(To Survivor),各存活物件年齡加1。最後將Eden和S0中的所有物件清除。

單核CPU, 1G記憶體,也能做JVM調優嗎?

GC後S0區域被清空。如上圖所示。S0和S1發生了互換,S1變成了From Survivor,S0變成了To Survivor。

注意,To Survivor區永遠都為空。這實際上是垃圾回收演算法-複製演算法在年輕代的實際應用。把年輕代分為Eden,S0,S1三個區域,每次垃圾回收時把可達物件複製到S0或S1,然後再清除掉Eden和(S1或S0)中的所有物件。由於每次GC時,新生代的可達物件非常少(絕大部分物件要被回收掉),一般不會超過新生代總體空間的10%,所以搜尋可達物件以及複製物件的成本都會非常低。而且這種複製的方式還能避免產生堆記憶體碎片,提高記憶體利用率。很多年輕代垃圾收集器都採用複製演算法,如ParNew。

單核CPU, 1G記憶體,也能做JVM調優嗎?

在程式執行過程中,新生代GC會反覆發生,長壽物件會在S0和S1之間反覆交換,年齡也會越來越大,當物件達到年齡上限時,會被晉升到老年代。這個年齡上限預設是15,可以透過引數-XX:MaxTenuringThreshold設定。如下圖,有些年輕代物件年齡達到了上限15,被轉移到了老年代。

單核CPU, 1G記憶體,也能做JVM調優嗎?

透過上面的圖文內容,我們瞭解了堆記憶體中物件的分配和流轉過程。那麼可以基於這些知識來做一些JVM調優的工作。

所謂

堆記憶體中物件的分配和流轉過程

,主要指的是降低Major GC(老年代GC)次數。記憶體只有1G,為了減少Major GC,最簡單的做法是適當調大老年代比例,但是老年代空間總有個上限,需要在老年代和年輕代之間找一個平衡點。

還可以適當調大MaxTenuringThreshold,來提高年輕代倖存區s0和s1的交換次數,進而減少物件晉升到老年代的機率。

另外調大幸存區比例,也可以減少基於動態物件年齡判定導致物件晉升老年代的機率。不管是哪種最佳化手段,都需要反覆調整和驗證(可以做效能測試驗證調整結果)。

再補充一個基礎知識點。Full GC,Major GC,Minor GC之間是什麼關係?

當前絕大部分垃圾收集器都採用分代回收的策略,年輕代和老年代的GC分別獨立進行。一般情況下,老年代Major GC是由年輕代Minor GC觸發的,Minor GC會導致部分存活時間較長的物件晉升到老年代,在晉升過程中如果老年代使用空間達到閾值就會發生Major GC。這種由Minor GC觸發Major GC引發整個堆記憶體GC的情況,我們一般稱之為Full GC。還有一些情況也會觸發Major GC,比如

大物件初始化時會跨過年輕代直接分配到老年代,這種情況觸發的Major GC和Minor GC就沒半點關係了。可以透過-XX:PretenureSizeThreshold引數設定大物件的大小,如果引數被設定成5MB,超過5MB的大物件會直接分配到老年代。

降低GC頻次

縮短GC時間和降低GC頻次,兩者是魚和熊掌的關係,不可兼得。如上面所說,在1G記憶體單核CPU的場景下,響應時間優先的CMS和G1都不適合。在垃圾收集器沒有太多選擇的情況下,如果想縮短Major GC時間,基本上只能減小老年代的比例了,老年代空間越小,每次Major GC需要處理的物件就越少,GC時間也就越短。老年代空間越小,GC的頻次自然也會更高,記憶體空間就那麼多,所以我們需要反覆試驗,在GC頻次和GC時間上找到最佳平衡點來滿足業務系統的要求。

縮短GC時間

JVM調優沒有什麼可以拿來即用的固定模板或規範,每個應用都有自己的獨特場景。不同的應用併發程度不一樣

對響應時間和吞吐量要求也不一樣

堆記憶體物件規模

物件生命週期

物件大小等等都不會完全一樣,這些因素都會影響到JVM的效能。所以,JVM調優是一個循序漸進的過程,必然需要經歷多次迭代,最終才能得到一個較好的折中方案。

作者簡介:

曾任職於阿里巴巴,每日優鮮等網際網路公司,任技術總監,15年電商網際網路經歷,擅長高併發場景下系統性能和穩定性解決方案