秒懂JVM的垃圾回收機制

 

前言

閱讀過王子之前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的分代模型

Tags: