018、大廠面試題:JVM中都有哪些常見的垃圾回收器,各自的特點是什么?

大廠面試題
JVM中都有哪些常見的垃圾回收器,各自的特點是什么?
1、前文回顧
上一篇文章我們給大家分析了一下到底什么時候會觸發(fā)Minor GC,什么時候會讓對象從新生代轉(zhuǎn)移到老年代,包括為了新生代轉(zhuǎn)移到老年代的內(nèi)存足夠安全,Minor GC之前要如何檢查老年代的內(nèi)存空間,在什么情況下會觸發(fā)老年代的Full GC,老年代的垃圾回收算法是什么,這些問題都已經(jīng)給大家分析清楚了。
這篇文章,我們先接著上篇文章,給大家來一個真實的我們之前一個生產(chǎn)系統(tǒng)的老年代頻繁Full GC的案例,讓大家更加透徹的理解整個對象分配以及轉(zhuǎn)移到老年代,以及Minor GC和Full GC的全過程。
2、一個日處理上億數(shù)據(jù)的計算系統(tǒng)
先給大家說一下這個系統(tǒng)的案例背景,大概來說是當時我們團隊里自己研發(fā)的一個數(shù)據(jù)計算系統(tǒng),日處理數(shù)據(jù)量在上億的規(guī)模。
為了方便大家集中注意力理解這個系統(tǒng)的生產(chǎn)環(huán)境的JVM相關(guān)的東西,所以對系統(tǒng)本身就簡化說明了。
簡單來說,這個系統(tǒng)就是會不停的從MySQL數(shù)據(jù)庫以及其他數(shù)據(jù)源里提取大量的數(shù)據(jù),加載到自己的JVM內(nèi)存里來進行計算處理,如下圖所示。

這個數(shù)據(jù)計算系統(tǒng)會不停的通過SQL語句和其他方式從各種數(shù)據(jù)存儲中提取數(shù)據(jù)到內(nèi)存中來進行計算,大致當時的生產(chǎn)負載是每分鐘大概需要執(zhí)行500次數(shù)據(jù)提取和計算的任務(wù)。
但是這是一套分布式運行的系統(tǒng),所以生產(chǎn)環(huán)境部署了多臺機器,每臺機器大概每分鐘負責執(zhí)行100次數(shù)據(jù)提取和計算的任務(wù)。
每次會提取大概1萬條左右的數(shù)據(jù)到內(nèi)存里來計算,平均每次計算大概需要耗費10秒左右的時間
然后每臺機器是4核8G的配置,JVM內(nèi)存給了4G,其中新生代和老年代分別是1.5G的內(nèi)存空間,大家看下圖。

3、這個系統(tǒng)到底多塊會塞滿新生代?
現(xiàn)在明確了一些核心數(shù)據(jù),接著我們來看看這個系統(tǒng)到底多快會塞滿新生代的內(nèi)存空間?
既然這個系統(tǒng)每臺機器上部署的實例,每分鐘會執(zhí)行100次數(shù)據(jù)計算任務(wù),每次是1萬條數(shù)據(jù)需要計算10秒的時間,那么我們來看看每次1萬條數(shù)據(jù)大概會占用多大的內(nèi)存空間?
這里每條數(shù)據(jù)都是比較大的,大概每條數(shù)據(jù)包含了平均20個字段,可以認為平均每條數(shù)據(jù)在1KB左右的大小。那么每次計算任務(wù)的1萬條數(shù)據(jù)就對應了10MB的大小。
所以大家此時可以思考一下,如果新生代是按照8:1:1的比例來分配Eden和兩塊Survivor的區(qū)域,那么大體上來說,Eden區(qū)就是1.2GB,每塊Survivor區(qū)域在100MB左右,如下圖。

基本上按照這個內(nèi)存大小而言,大家會發(fā)現(xiàn),每次執(zhí)行一個計算任務(wù),就會在Eden區(qū)里分配10MB左右的對象,那么一分鐘大概對應100次計算任務(wù)
其實基本上一分鐘過后,Eden區(qū)里就全是對象,基本就全滿了。
所以說,回答這個小節(jié)的問題,新生代里的Eden區(qū),基本上1分鐘左右就迅速填滿了。
4、觸發(fā)Minor GC的時候會有多少對象進入老年代?
此時假設(shè)新生代的Eden區(qū)在1分鐘過后都塞滿對象了,然后在接著繼續(xù)執(zhí)行計算任務(wù)的時候,勢必會導致需要進行Minor GC回收一部分的垃圾對象。
那么上篇文章給大家講過這里在執(zhí)行Minor GC之前會先進行的檢查。
首先第一步,先看看老年代的可用內(nèi)存空間是否大于新生代全部對象?
看下圖,此時老年代是空的,大概有1.5G的可用內(nèi)存空間,新生代的Eden區(qū)大概算他有1.2G的對象好了。

此時會發(fā)現(xiàn)老年代的可用內(nèi)存空間有1.5GB,新生代的對象總共有1.2GB,即使一次Minor GC過后,全部對象都存活,老年代也能放的下的,那么此時就會直接執(zhí)行Minor GC了。
那么此時Eden區(qū)里有多少對象還是存活的,無法被垃圾回收呢?
大家可以考慮一下之前說的那個點,每個計算任務(wù)1萬條數(shù)據(jù)需要計算10秒鐘,假設(shè)此時80個計算任務(wù)都執(zhí)行結(jié)束了,但是還有20個計算任務(wù)共計200MB的數(shù)據(jù)還在計算中,此時就是200MB的對象是存活的,不能被垃圾回收掉,然后有1GB的對象是可以垃圾回收的
大家看下圖。

此時一次Minor GC就會回收掉1GB的對象,然后200MB的對象能放入Survivor區(qū)嗎?
不能!因為任何一塊Survivor區(qū)實際上就100MB的空間,此時就會通過空間擔保機制,讓這200MB對象直接進入老年代去,占用里面200MB內(nèi)存空間,然后Eden區(qū)就清空了
大家看下圖。

5、系統(tǒng)運行多久,老年代大概就會填滿?
那么大家想一下,這個系統(tǒng)大概運行多久,老年代會填滿呢?
按照上述計算,每分鐘都是一個輪回,大概算下來是每分鐘都會把新生代的Eden區(qū)填滿,然后觸發(fā)一次Minor GC,然后大概都會有200MB左右的數(shù)據(jù)進入老年代。
那么大家可以想一下,假設(shè)現(xiàn)在2分鐘運行過去了,此時老年代已經(jīng)有400MB內(nèi)存被占用了,只有1.1GB的內(nèi)存可用,此時如果第3分鐘運行完畢,又要進行Minor GC,會做什么檢查呢?如下圖。

此時會先檢查老年代可用空間是否大于新生代全部對象?
此時老年代可用空間1.1GB,新生代對象有1.2GB,那么此時假設(shè)一次Minor GC過后新生代對象全部存活,老年代是放不下的,那么此時就得看看一個參數(shù)是否打開了 。
如果“-XX:-HandlePromotionFailure”參數(shù)被打開了,當然一般都會打開,此時會進入第二步檢查,就是看看老年代可用空間是否大于歷次Minor GC過后進入老年代的對象的平均大小。
我們已經(jīng)計算過了,大概每分鐘會執(zhí)行一次Minor GC,每次大概200MB對象會進入老年代。
那么此時發(fā)現(xiàn)老年代的1.1GB空間,是大于每次Minor GC后平均200MB對象進入老年代的大小的
所以基本可以推測,本次Minor GC后大概率還是有200MB對象進入老年代,1.1G可用空間是足夠的。
所以此時就會放心執(zhí)行一次Minor GC,然后又是200MB對象進入老年代。
轉(zhuǎn)折點大概在運行了7分鐘過后,7次Minor GC執(zhí)行過后,大概1.4G對象進入老年代,老年代剩余空間就不到100MB了,幾乎快滿了
如下圖:

6、這個系統(tǒng)運行多久,老年代會觸發(fā)1次Full GC?
大概在第8分鐘運行結(jié)束的時候,新生代又滿了,執(zhí)行Minor GC之前進行檢查,此時發(fā)現(xiàn)老年代只有100MB內(nèi)存空間了,比之前每次Minor GC后進入老年代的200MB對象要小,此時就會直接觸發(fā)一次Full GC。
Full GC會把老年代的垃圾對象都給回收了,假設(shè)此時老年代被占據(jù)的1.4G空間里,全部都是可以回收的對象,那么此時一次性就會把這些對象都給回收了,如下圖。

然后接著就會執(zhí)行Minor GC,此時Eden區(qū)情況,200MB對象再次進入老年代,之前的Full GC就是為這些新生代本次Minor GC要進入老年代的對象準備的,如下圖。

按照這個運行模型,基本上平均就是七八分鐘一次Full GC,這個頻率就相當高了。因為每次Full GC速度都是很慢的,性能很差,而且明天的文章會告訴大家,為什么Full GC的時候會嚴重影響系統(tǒng)性能。
7、該案例應該如何進行JVM優(yōu)化?
相信通過這個案例,大家結(jié)合圖一路看下來,對新生代和老年代如何配合使用,然后什么情況下觸發(fā)Minor GC和Full GC,什么情況下會導致頻繁的Minor GC和Full GC,大家都有了更加深層次和透徹的理解了。
對這個系統(tǒng),其實要優(yōu)化也是很簡單的,因為這個系統(tǒng)是數(shù)據(jù)計算系統(tǒng),每次Minor GC的時候,必然會有一批數(shù)據(jù)沒計算完畢
但是按照現(xiàn)有的內(nèi)存模型,最大的問題,其實就是每次Survivor區(qū)域放不下存活對象。
所以當時我們就是對生產(chǎn)系統(tǒng)進行了調(diào)整,增加了新生代的內(nèi)存比例,3GB左右的堆內(nèi)存,其中2GB分配給新生代,1GB留給老年代
這樣Survivor區(qū)大概就是200MB,每次剛好能放得下Minor GC過后存活的對象了,如下圖所示。

只要每次Minor GC過后200MB存活對象可以放Survivor區(qū)域,那么等下一次Minor GC的時候,這個Survivor區(qū)的對象對應的計算任務(wù)早就結(jié)束了,都是可以回收的了
此時比如Eden區(qū)里1.6GB空間被占滿了,然后Survivor1區(qū)里有200MB上一輪 Minor GC后存活的對象,如下圖。

然后此時執(zhí)行Minor GC,就會把Eden區(qū)里1.6GB對象回收掉,Survivor1區(qū)里的200MB對象也會回收掉,然后Eden區(qū)里剩余的200MB存活對象會放入Survivor2區(qū)里,如下圖。

以此類推,基本上就很少對象會進入老年代中,老年代里的對象也不會太多的。
通過這個分析和優(yōu)化,定時我們成功的把生產(chǎn)系統(tǒng)的老年代Full GC的頻率從幾分鐘一次降低到了幾個小時一次,大幅度提升了系統(tǒng)的性能,避免了頻繁Full GC對系統(tǒng)運行的影響。
但是大家在這里肯定注意到一點,就是之前說過一個動態(tài)年齡判定升入老年代的規(guī)則,就是如果Survivor區(qū)中的同齡對象大小超過Survivor區(qū)內(nèi)存的一半,就要直接升入老年代。所以這里優(yōu)化的方式僅僅是做一個示例說明,意思是要增加Survivor區(qū)的大小,讓Minor GC后的對象進入Survivor區(qū)中,避免進入老年代。
實際上為了避免動態(tài)年齡判定規(guī)則把Survivor區(qū)中的對象直接升入老年代,在這里如果新生代內(nèi)存有限,那么可以調(diào)整"-XX:SurvivorRatio=8"這個參數(shù),默認是說Eden區(qū)比例為80%,也可以降低Eden區(qū)的比例,給兩塊Survivor區(qū)更多的內(nèi)存空間,然后讓每次Minor GC后的對象進入Survivor區(qū)中,還可以避免動態(tài)年齡判定規(guī)則直接把他們升入老年代。
8、垃圾回收器簡介
在新生代和老年代進行垃圾回收的時候,都是要用垃圾回收器進行回收的,不同的區(qū)域用不同的垃圾回收器。
垃圾回收器是下周和下下周的重點內(nèi)容,到時候會深入分析我們常用的ParNew、CMS和G1三種垃圾回收器的工作原理和優(yōu)缺點。
這篇文章先簡單給大家介紹一下:
Serial和Serial Old垃圾回收器:分別用來回收新生代和老年代的垃圾對象
工作原理就是單線程運行,垃圾回收的時候會停止我們自己寫的系統(tǒng)的其他工作線程,讓我們系統(tǒng)直接卡死不動,然后讓他們垃圾回收,這個現(xiàn)在一般寫后臺Java系統(tǒng)幾乎不用。
ParNew和CMS垃圾回收器:ParNew現(xiàn)在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他們都是多線程并發(fā)的機制,性能更好,現(xiàn)在一般是線上生產(chǎn)系統(tǒng)的標配組合。下周會著重分析這兩個垃圾回收器。
G1垃圾回收器:統(tǒng)一收集新生代 和老年代,采用了更加優(yōu)秀的算法和設(shè)計機制,是下下周的重點,一周都會來分析G1垃圾回收器的工作原理和優(yōu)缺點。
大家本周的重點,就是透徹理解新生代和老年代的對象分配以及垃圾回收的觸發(fā)時機和運行機制,然后簡單了解有哪些垃圾回收器即可。
明天會給大家講垃圾回收最讓人討厭的Stop The World是怎么回事。
經(jīng)過本周的學習,相信大家從原理層面對JVM有一個更加深入的認識。
9、昨日思考題
到底什么時候會嘗試觸發(fā)Minor GC?
觸發(fā)Minor GC之前會如何檢查老年代大小,涉及哪幾個步驟和條件?
什么時候在Minor GC之前就會提前觸發(fā)一次Full GC?
Full GC的算法是什么?
Minor GC過后可能對應哪幾種情況?
哪些情況下Minor GC后的對象會進入老年代?
昨天讓大家把這個過程自己詳細的梳理出來,相信看了今天的文章,理解的就更加透徹了。
10、今日小作業(yè)
本文是一個非常經(jīng)典的真實生產(chǎn)案例和優(yōu)化實踐經(jīng)驗,建議大家不要光看,自己把今天的案例,從背景到分析到解決,一步一步自己畫圖來推演一遍,徹底吃透這個案例。
這對大家以后分析更多的JVM案例和優(yōu)化,有非常好的作用。
End
版權(quán):公眾號儒猿技術(shù)窩
未經(jīng)許可不得傳播,如有侵權(quán)將追究法律責任