java 六 Young GC 和 Full GC


糟糕!運行着的線上系統突然卡死無法訪問,萬惡的JVM GC!

基於JVM運行的系統最怕什么?

在JVM運行的時候,最核心的內存區域,其實就是堆內存,在這里會放各種我們系統中創建出來的對象。
而且堆內存里通常都會划分為新生代和老年代兩個內存區域,對象一般來說都是優先放在新生代的。在年輕代(也可以叫做新生代)快要塞滿的時候,就會觸發年輕代gc,也就是對年輕代進行垃圾回收,需要把年輕代里的垃圾對象都給回收掉。JVM 通過復制算法進行回收,通常來說新生代會有一塊Eden區域用來創建對象,默認占據80%的內存,還有兩塊Survivor區域用來放垃圾回收后存活下來的對象,分別占據10%的內存。
而且大家要注意一點,一旦要對新生代進行垃圾回收了,此時一定會停止系統程序的運行,不讓系統程序執行任何代碼邏輯了,這個叫做“Stop the World” 此時只能允許后台的垃圾回收器的多個垃圾回收線程去工作,執行垃圾回收。

要給大家說第一個重點了,不知道大家發現了沒有,這里有一個很大的問題,就是每次一旦年輕代塞滿之后,在進行垃圾回收的時候,這個期間都必須停止系統程序的運行!這個就是基於JVM運行的系統最害怕的問題:系統卡頓問題!

年輕代gc到底多久一次對系統影響不大?

其實通常來說是不大的,不知道大家發現沒有,其實年輕代gc幾乎沒什么好調優的,因為他的運行邏輯非常簡單,就是Eden一旦滿了無法放新對象就觸發一次gc。
一般來說,真要說對年輕代的gc進行調優,只要你給系統分配足夠的內存即可,核心點還是在於堆內存的分配、新生代內存的分配
內存足夠的話,通常來說系統可能在低峰時期在幾個小時才有一次新生代gc,高峰期最多也就幾分鍾一次新生代gc。
而且新生代采用的復制算法效率極高,因為新生代里存活的對象很少,只要迅速標記出這少量存活對象,移動到Survivor區,然后回收掉其他全部垃圾對象即可,速度很快。
很多時候,一次新生代gc可能也就耗費幾毫秒,幾十毫秒。大家設想一下,假如說你的系統運行着,然后每隔幾分鍾或者幾十分鍾執行一次新生代gc,系統卡頓幾十毫秒,就這期間的請求會卡頓幾十毫秒,幾乎用戶都是無感知的,所以新生代gc一般基本對系統性能影響不大。

什么時候新生代gc對系統影響很大?

當你的系統部署在大內存機器上的時候,比如說你的機器是32核64G的機器,此時你分配給系統的內存有幾十個G,新生代的Eden區可能30G~40G的內存。
比如類似Kafka、Elasticsearch之類的大數據相關的系統,都是部署在大內存的機器上的,此時如果你的系統負載非常的高,對於大數據系統是很有可能的,比如每秒幾萬的訪問請求到Kafka、Elasticsearch上去。
那么可能導致你Eden區的幾十G內存頻繁塞滿要觸發垃圾回收,假設1分鍾會塞滿一次。
然后每次垃圾回收要停頓掉Kafka、Elasticsearch的運行,然后執行垃圾回收大概需要幾秒鍾,此時你發現,可能每過一分鍾,你的系統就要卡頓幾秒鍾,有的請求一旦卡死幾秒鍾就會超時報錯,此時可能會導致你的系統頻繁出錯。

如何解決大內存機器的新生代GC過慢的問題?

那么如何解決這種幾十G的大內存機器的新生代GC過慢的問題呢?
用G1垃圾回收器
大家都知道,我們針對G1垃圾回收器,可以設置一個期望的每次GC的停頓時間,比如我們可以設置一個20ms。
那么G1基於他的Region內存划分原理,就可以在運行一段時間之后,比如就針對2G內存的Region進行垃圾回收,此時就僅僅停頓20ms,然后回收掉2G的內存空間,騰出來了部分內存,接着還可以繼續讓系統運行。
G1天生就適合這種大內存機器的JVM運行,可以完美解決大內存垃圾回收時間過長的問題。

要命的頻繁老年代gc問題

其實新生代gc一般問題不會太大,但是真正問題最大的地方,在於頻繁觸發老年代的GC。
之前給大家講過對象進入老年代的幾個條件:年齡太大了、動態年齡判斷規則、新生代gc后存活對象太多無法放入Survivor中。
給大家重新分析一下這幾個條件。

  • 第一個,對象年齡太大了,這種對象一般很少,都是系統中確實需要長期存在的核心組件,他們一般不需要被回收掉,所以在新生代熬過默認15次垃圾回收之后就會進入老年代。
  • 第二個,動態年齡判定規則,如果一次新生代gc過后,發現Survivor區域中的幾個年齡的對象加起來超過了Survivor區域的50%,比如說年齡1+年齡2+年齡3的對象大小總和,超過了Survivor區域的50%,此時就會把年齡3以上的對象都放入老年代。
  • 第三個,新生代垃圾回收過后,存活對象太多了,無法放入 Surviovr中,此時直接進入老年代。

其實上述條件中,第二個和第三個都是很關鍵的,通常如果你的新生代中的Survivor區域內存過小,就會導致上述第二個和第三個條件頻繁發生,然后導致大量對象快速進入老年代,進而頻繁觸發老年代的gc,如下圖。

老年代gc通常來說都很耗費時間,無論是CMS垃圾回收器還是G1垃圾回收器,因為比如說CMS就要經歷初始標記、並發標記、重新標記、並發清理、碎片整理幾個環節,過程非常的復雜,G1同樣也是如此。
通常來說,老年代gc至少比新生代gc慢10倍以上,比如新生代gc每次耗費200ms,其實對用戶影響不大,但是老年代每次gc耗費2s,那可能就會導致老年代gc的時候用戶發現頁面上卡頓2s,影響就很大了。
所以一旦你因為jvm內存分配不合理,導致頻繁進行老年代gc,比如說幾分鍾就有一次老年代gc,每次gc系統都停頓幾秒鍾,那簡直對你的系統就是致命的打擊。此時用戶會發現頁面上或者APP上經常性的出現點擊按鈕之后卡頓幾秒鍾。

大廠面試題:Young GC和Full GC分別在什么情況下會發生?

Young GC的觸發時機

Young GC 其實一般就是在新生代的Eden區域滿了之后就會觸發,采用復制算法來回收新生代的垃圾

Old GC和Full GC的觸發時機

其實之前的文章里也對Old GC的觸發時機說的很清晰了,簡而言之就是下面幾種情況:
1.發生Young GC之前進行檢查,如果“老年代可用的連續內存空間” < “新生代歷次Young GC后升入老年代的對象總和的平均大小”,說明本次Young GC后可能升入老年代的對象大小,可能超過了老年代當前可用內存空間。
此時必須先觸發一次Old GC給老年代騰出更多的空間,然后再執行Young GC
2.執行Young GC之后有一批對象需要放入老年代,此時老年代就是沒有足夠的內存空間存放這些對象了,此時必須立即觸發一次Old GC
3.老年代內存使用率超過了92%,也要直接觸發Old GC,當然這個比例是可以通過參數調整的
其實說白了,上述三個條件你概括成一句話,就是老年代空間也不夠了,沒法放入更多對象了,這個時候務必執行Old GC對老年代進行垃圾回收。
順便說一句,大家在很多地方看到一個說法,意思是說Old GC執行的時候一般都會帶上一次Young GC
可能很多人不理解,其實如果你把咱們這里的幾個條件分析清楚了就知道了,一般Old GC很可能就是在Young GC之前觸發或者在Young GC之后觸發的,所以自然Old GC一般都會跟一次Young GC連帶關聯在一起了。
另外一個,在很多JVM的實現機制里,其實在上述幾種條件達到的時候,他觸發的實際上就是Full GC,這個Full GC會包含Young GC、Old GC和永久代的GC
也就是說觸發Full GC的時候,可能就會去回收年輕代、老年代和永久代三個區域的垃圾對象。

永久代滿了之后怎么辦?

大家現在既然都知道了,Full GC有上述幾個觸發條件,同時觸發Full GC的時候其實會帶上針對新生代的Young GC,也會有針對老年代的Full GC,還會有針對永久代的GC。所以假如存放類信息、常量池的永久代滿了之后,就會觸發一次Full GC。

這樣Full GC執行的時候,就會順帶把永久代中的垃圾給回收了,但是永久代中的垃圾一般是很少的,因為里面存放的都是一些類,還有常量池之類的東西,這些東西通常來說是不需要回收的。如果永久代真的放滿了,回收之后發現沒騰出來更多的地方,此時只能拋出內存不夠的異常了。

案例實戰:每秒10萬並發的BI系統是如何頻繁發生Young GC的?

剛開始的時候,這個BI系統使用的商家是不多的。因為大家要知道,即使在一個龐大的互聯網大廠里,雖然說大廠本身積累了大量的商家,但是你要是針對他們上線一個付費的產品,剛開始未必所有人都買賬,所以一開始系統上線大概就少數商家在使用,比如就幾千個商家。
所以剛開始系統部署的非常簡單,就是用幾台機器來部署了上述的BI系統,機器都是普通的4核8G的配置,然后在這個配置之下,一般來說給堆內存中的新生代分配的內存都在1.5G左右,Eden區大概也就1G左右的空間。其實剛開始,在少數商家的量級之下,這個系統是沒多大問題的,運行的非常良好,但是問題恰恰就出在突然使用系統的商家數量開始暴漲的時候。

沒什么大影響的頻繁Young GC

根據我們之前的測算,每個請求大概需要加載出來100kb的數據進行計算,因此每秒500個請求,就需要加載出來50MB的數據到內存中進行計算,只要區區200s,也就是3分鍾左右的時間,就會迅速填滿Eden區,然后觸發一次Young GC對新生代進行垃圾回收。

當然1G左右的Eden進行Young GC其實速度相對是比較快的,可能也就幾十ms的時間就可以搞定了,其實對系統性能影響並不大。而且上述BI系統場景下,基本上每次Young GC后存活對象可能就幾十MB,甚至是幾MB。所以如果僅僅只是這樣的話,那么大家可能會看到如下場景,BI系統運行幾分鍾過后,就會突然卡頓個10ms,但是對終端用戶和系統性能幾乎是沒有影響的

提升機器配置:運用大內存機器

針對這樣的一套系統,后來隨着越來越多的商家來使用,並發壓力越來越大,甚至高峰期會有每秒10萬的並發壓力
大家想想,如果還是用4核8G的機器來支撐,那么可能需要部署上百台機器來抗住每秒10萬的高並發壓力。
所以一般針對這種情況,我們會提升機器的配置,本身BI系統就是非常吃內存的系統,所以我們將部署的機器全面提升到了16核32G的高配置機器上去。每台機器可以抗個每秒幾千請求,此時只要部署比如二三十台機器就可以了。
但是此時問題就來了,大家可以想一下,如果要是用大內存機器的話,那么新生代至少會分配到20G的大內存,Eden區也會占據16G以上的內存空間,此時如下圖所示。

此時每秒幾千請求的話,每秒大概會加載到內存中幾百MB的數據,那么大概可能幾十秒,甚至1分鍾左右就會填滿Eden區,會就需要執行Young GC。
此時Young GC要回收那么大的內存,速度會慢很多,也許此時就會導致系統卡頓個幾百毫秒,或者1秒鍾。那么你要是系統卡頓時間過長,必然會導致瞬間很多請求積壓排隊,嚴重的時候會導致線上系統時不時出現前端請求超時的問題,就是前端請求之后發現一兩秒后還沒返回就超時報錯了。

用G1來優化大內存機器的Young GC性能

所以當時對這個系統的一個優化,就是采用G1垃圾回收器來應對大內存的Young GC過慢的問題
對G1設置一個預期的GC停頓時間,比如100ms,讓G1保證每次Young GC的時候最多停頓100ms,避免影響終端用戶的使用。
此時效果是非常顯著的,G1會自動控制好在每次Young GC的時候就回收一部分Region,確保GC停頓時間控制在100ms以內
這樣的話,也許Young GC的頻率會更高一些,但是每次停頓時間很小,這樣對系統影響就不大了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM