秒懂JVM的垃圾回收機制

王子發表於2020-10-14

 

前言

閱讀過王子之前JVM文章的小夥伴們,應該已經對JVM的記憶體分佈情況有了一個清晰的認識了,今天我們就接著來聊聊JVM的垃圾回收機制,讓小夥伴們輕鬆理解JVM是怎麼進行垃圾回收的。

 

複製演算法、Eden區和Survivor區

首先我們就來探索一下對於JVM堆記憶體中的新生代區域,是怎麼進行垃圾回收的。

實際上JVM是把新生代分為三塊區域的:1個Eden區,2個Survivor區

其中Eden區佔用80%的記憶體空間,每塊Survivor各佔用10%的記憶體空間。比如Eden區有800M,那麼每個Survivor區就有100M。

平時可以使用的區域是Eden區和其中一塊Survivor區,也就是900M的記憶體空間。

 

 

剛開始建立物件的時候,物件都是分配在Eden區中的,如果Eden區快滿了,就會觸發垃圾回收 Young GC,使用的就是複製演算法進行垃圾回收,流程如下:

首先會把Eden區中的存活物件一次性轉入其中一塊空著的Survivor區中。

然後清空Eden區,之後新建立的物件就再次被放入了Eden區中了。

如果下次Eden區快滿了,就會再次觸發Young GC,這個時候會把Eden區和存在物件的Survivor區中存活的物件轉移到另一塊空著的Survivor區中,並清空Eden區和之前存在物件的Survivor區。

這就是複製演算法的流程。

一直要保持一個Survivor區是空的以供複製演算法垃圾回收,而這塊區域只佔用整個記憶體的10%,其他90%的記憶體都能被使用,可見記憶體利用率還是相當高的。

 

什麼時候進入老年代

接下來我們就來看一下什麼時候會進入老年代,這個問題上篇文章輕鬆理解JVM的分代模型中已經簡單的介紹過了,今天會對此展開進行詳細探索。

 

1.躲過15次GC後進入老年代

在預設的情況下,如果新生代中的某個物件經歷了15次GC後,還是沒有被回收掉,那麼它就會被轉入到老年代中。

這個具體躲過多少次,是可以自己設定的,通過JVM引數“-XX:MaxTenuringThreshold”來設定,預設是15.

2.動態物件年齡判斷

另一種判斷方式也可以進入老年代,是不用等待GC15次的。

它的大致規則是,假如一批物件總大小大於了當前Survivor區域記憶體的大小的50%,那麼大於等於這批物件年齡的物件就會被轉移到老年代。

小夥伴們可能覺得有些沒看明白這句話的意思,沒關係,我們看一下圖

 

 

 假設Survivor中有兩個物件,它們都經歷過2次GC,年齡是2歲,而且兩個物件加在一起的大小大於50M,也就是超過了Survivor區域記憶體大小的50%,那麼這個時候,Survivor區域中年齡大於等於2歲的物件就要全部轉移到老年代中。

這就是所謂的動態年齡判斷規則。

要注意的是,年齡1+年齡2+年齡n的多個年齡物件大小超過Survivor區的50%,此時會把年齡n以上的物件放入老年代。

3.大物件直接進入老年代

有一個JVM引數"-XX:PretenureSizeThreshold",預設值是0,表示任何情況都先把物件分配給Eden區。

我們可以給他設定一個位元組數1048576位元組,也就是1M。

它的意思就是當要建立的物件大於1M的時候,就會直接把這個物件放入到老年代中,壓根不會經過新生代。

因為大物件在經歷複製演算法進行GC的時候是會降低效能的,所以直接放入老年代就可以了。

4.Young GC後存活的物件太多無法放入Survivor區

還有一種情況,就是Young GC後存活的物件太多,Survivor區放不下了,這個時候就會把這些物件直接轉移到老年代中。

這裡我們就要思考一個問題了,如果老年代也放不下了怎麼辦呢?

 

老年代空間分配擔保原則

首先,在執行任何一次Young GC之前,JVM都會先檢查一下老年代可用的記憶體空間是否大於新生代所有物件的總大小。

為啥要檢查這個呢?因為在極端情況下,Young GC後,新生代中所有的物件都存活下來了,那就會把所有新生代中的物件放入老年代中。

如果說老年代可用記憶體大於新生代物件總大小,那麼就可以放心的執行Young GC了。

但是如果老年代的可用記憶體小於新生代物件的總大小,這個時候就會看一個引數“-XX:HandlePromotionFailure”是否設定為true了(可以認為jdk7之後,預設設定為true)。

如果設定為true,那麼進入下一步判斷,就是看看老年代可用的記憶體,是否大於之前每次Young GC後進入老年代物件的平均大小

如果說老年代的可用記憶體小於平均大小,或者說引數沒有設定成true,那麼就會直接觸發“Full GC”,就是對老年代進行垃圾回收,騰出空間後,再進行Young GC。

 

如果上邊兩種情況判斷成功,沒有執行Full GC,進行了Young GC,有以下幾種可能:

1.如果Young GC後,存活的物件大小小於Survivor區域的大小,那麼直接進入Survivor區域即可。

2.如果Young GC後,存活的物件大小大於Survivor區域的大小,但是小於老年代可用記憶體大小,那就直接進入老年代。

3.很不幸,老年代可用空間也放不下這些存活物件了,那就會發生“Handle Promotion Failure”的情況,觸發Full GC。

 

如果Full GC後,老年代可用記憶體還是不夠,那麼就會導致OOM記憶體溢位了。

這段內容可能比較繁瑣,結合記憶體模型,多看兩遍相信小夥伴們是可以讀懂的。

 

 

老年代的垃圾回收演算法

接下來我們就來介紹一下老年代的垃圾回收演算法,標記整理演算法,理解起來還是比較容易的。

 

 

開始時我們的物件是胡亂分佈的,經過垃圾回收後,會標記出哪些是存活物件,哪些是垃圾物件,而後會把這些存活物件在記憶體中進行整理移動,儘量都挪到一邊去靠在一起,然後再把垃圾物件進行清除,這樣做的好處就是避免了垃圾回收後產生大片的記憶體碎片。

但是這一過程其實是比較耗時的,至少要比新生代的垃圾回收演算法慢10倍。

所以如果系統頻繁出現Full GC,會嚴重影響系統效能,出現頻繁卡頓。

所以JVM優化的一大問題就是減少Full GC頻率。

 

垃圾回收器

新生代和老年代進行垃圾回收的時候是通過不同的垃圾回收器進行回收的。

Seral和Seral Old垃圾回收器:分別用於回收新生代和老年代。

工作原理是單執行緒執行,垃圾回收的時候會停止我們系統的其他執行緒,讓系統卡死不動,然後執行垃圾回收,這個現在基本已經不會使用了

ParNew和CMS垃圾回收器:分別用於回收新生代和老年代。

它們都是多執行緒併發的,效能更好,現在一般是線上生產系統的標配。

G1垃圾回收器:統一收集新生代和老年代,採用了更加優秀的演算法機制。

 

這裡只是給大家做一下簡單的介紹,更詳細的內容以後文章會單獨解析。

 

Stop the World

JVM最大的痛點就是Stop the World了。

在垃圾回收的時候,儘可能的要讓垃圾回收器專心的去做垃圾回收的操作(防止垃圾回收的時候還在建立新物件,那不就亂套了嗎),所以此時JVM會在後臺進入Stop the World狀態。

進入這個狀態後,會直接停止我們系統的工作執行緒,讓我們的程式碼不在執行。

接著垃圾回收完成後,會恢復工作執行緒,程式碼就可以繼續執行了。

所以說只要是經歷GC,其實就會讓系統卡死一段時間,新生代的垃圾回收可能感受不到太多,單老年代的垃圾回收耗時更多,可能會明顯的感覺到系統的卡死。

所以說無論是新生代的垃圾回收還是老年代的垃圾回收,我們都應該儘量的減少它們的頻率。

 

總結

今天的乾貨內容還是比較多的,相信小夥伴們閱讀後對JVM會有一個更深的瞭解。

建議小夥伴們自己找資料瞭解一下幾種垃圾回收器的實現原理,我們之後的文章會陸續介紹。

好了,那就到這裡了,歡迎評論區留言討論。你的支援就是我更新的動力!

 

 

往期文章推薦:

大白話談JVM的類載入機制

JVM記憶體模型不再是祕密

輕鬆理解JVM的分代模型

相關文章