天天看點

年輕代和老年代分别适合什麼樣的垃圾回收算法

年輕代

1. 複制算法的背景引入

  針對新生代的垃圾回收算法,他叫做複制算法

  簡單來說,就是如下圖所示,首先把新生代的記憶體分為兩塊。

年輕代和老年代分别适合什麼樣的垃圾回收算法

  接着假設有如下代碼,在“loadReplicasFromDisk()”方法中建立了對象,此時對象就就會配置設定在新生代其中一塊記憶體空間裡。而且是由“main線程”的棧記憶體中的“loadReplicasFromDisk()”方法的棧幀内的局部變量來引用的,如下圖所示。

年輕代和老年代分别适合什麼樣的垃圾回收算法
年輕代和老年代分别适合什麼樣的垃圾回收算法

  接着大家想象一下,假設與此同時,代碼在不停的運作,然後大量的對象都配置設定在了新生代記憶體的其中一塊記憶體區域裡,也隻會配置設定在那塊區域裡,而且配置設定過後,很快就失去了局部變量或者類靜态變量的引用,成為了垃圾對象

此時如下圖所示。

年輕代和老年代分别适合什麼樣的垃圾回收算法

接着這個時候,新生代記憶體那塊被配置設定對象的記憶體區域基本都快滿了,再次要配置設定對象的時候,發現裡面記憶體空間不足了。

那麼此時就會觸發Minor GC去回收掉新生代那塊被使用的記憶體空間的垃圾對象。

那麼回收的時候是怎麼做的呢?

2. 一種不太好的垃圾回收思路

假設現在采用的垃圾回收思路,就是直接對上圖中被使用的那塊記憶體區域中的垃圾對象進行标記

也就是根據上篇文章講的那套思路,标記出哪些對象是可以被垃圾回收的,然後就直接對那塊記憶體區域中的對象進行垃圾回收,把記憶體空出來。大家想想,這種思路好嗎?

這種思路去垃圾回收,可能會在回收完畢之後造成那塊記憶體區域看起來跟下圖一樣。

年輕代和老年代分别适合什麼樣的垃圾回收算法

看上面的圖,不知道大家發現什麼沒有,在那塊被使用的記憶體區域裡,回收掉了大量的垃圾對象,但是保留了一些被

人引用的存活對象

但是呢,存活對象在記憶體區域裡東一個西一個,非常的淩亂,而且造成了大量的記憶體碎片。

那麼什麼是記憶體碎片呢?

我們再看下面的圖我用紅線标記出來的區域,那些就是所謂的記憶體碎片。

年輕代和老年代分别适合什麼樣的垃圾回收算法

看到了嗎?在各種淩亂的存活對象的中間,出現了大量的紅圈圈出來的記憶體碎片

這些記憶體碎片的大小不一樣,有的可能很大,有的可能很小。

那麼記憶體碎片太多會造成什麼問題呢?記憶體浪費

啥意思?比如現在打算配置設定一個新的對象,嘗試在上圖那塊被使用的記憶體區域裡去配置設定

此時如下圖所示,可能因為記憶體碎片太多的緣故,雖然所有的記憶體碎片加起來其實有很大的一塊記憶體,但是因為這些

年輕代和老年代分别适合什麼樣的垃圾回收算法

記憶體都是碎片式分散的,是以導緻沒有一塊完整的足夠的記憶體空間來配置設定新的對象。

是以這種直接對一塊記憶體空間回收掉垃圾對象,保留存活對象的方法,絕對是不可取的

因為記憶體碎片太多,就是他最大的問題,會造成大量的記憶體浪費,很多記憶體碎片壓根兒是沒法使用的。

3.一個合理的垃圾回收思路

那麼能不能用一種合理的思路來進行垃圾回收呢?

可以!這個時候上圖中一直沒派上用場的另外一塊空白的記憶體區域就出場了。

首先,并不是按照上述思路直接對已經使用的那塊記憶體把垃圾對象全部回收掉,然後保留全部存活對象。

而是先對那塊在使用的記憶體空間标記出裡面哪些對象是不能進行垃圾回收的,就是要存活的對象

然後先把那些存活的對象轉移到另外一塊空白的記憶體中,如下圖。不知道大家發現這裡的玄機沒有?

年輕代和老年代分别适合什麼樣的垃圾回收算法

沒錯,通過把存活對象先轉移到另外一塊空白記憶體區域,我們可以把這些對象都比較緊湊的排列在記憶體裡

這樣就可以讓被轉移的那塊記憶體區域幾乎沒有什麼記憶體碎片,對象都是按順序排列在這塊記憶體裡的。

然後那塊被轉移的記憶體區域,是不是多出來一大塊連續的可用的記憶體空間?

此時就可以将新對象配置設定在那塊連續記憶體空間裡了,如下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

這個時候,再一次性把原來使用的那塊記憶體區域中的垃圾對象全部一掃而空,全部給回收掉,空出來一塊記憶體區域,

如下圖。這就是所謂的“複制算法“,把新生代記憶體劃分為兩塊記憶體區域,然後隻使用其中一塊記憶體

年輕代和老年代分别适合什麼樣的垃圾回收算法

待那塊記憶體快滿的時候,就把裡面的存活對象一次性轉移到另外一塊記憶體區域,保證沒有記憶體碎片

接着一次性回收原來那塊記憶體區域的垃圾對象,再次空出來一塊記憶體區域。兩塊記憶體區域就這麼重複着循環使用。

4.複制算法有什麼缺點?

複制算法的缺點其實非常的明顯,如果按照上述的思路,大家會發現,假設我們給新生代1G的記憶體空間,那麼隻有512MB的記憶體空間是可以用的,另外512MB的記憶體空間是一直要放在那裡空着的,然後512MB記憶體空間滿了,就把存活對象轉移到另外一塊512MB的記憶體空間去

從始至終,就隻有一半的記憶體可以用,這樣的算法顯然對記憶體的使用效率太低了。

5.複制算法的優化:Eden區和Survivor區

之前我給大家分析過,系統運作時,對JVM記憶體的使用模型,大體上就是我們的代碼不停的建立對象然後配置設定在新生代裡,但是一般很快那個對象就沒人引用了,成了垃圾對象。

接着一段時間過後,新生代就滿了,此時就會回收掉那些垃圾對象,空出來記憶體空間,給後續其他的對象來使用。

但是我們之前分析過,其實絕大多數的對象都是存活周期非常短的對象,可能被建立出來1毫秒之後就沒人引用了,他就是垃圾對象了。

是以大家可以想象一下,可能一次新生代垃圾回收過後,99%的對象其實都被垃圾回收了,就1%的對象存活了下來,可能就是一些長期存活的對象,或者還沒使用完的對象。是以實際上真正的複制算法會做出如下優化,把新生代記憶體區域劃分為三塊:

1個Eden區,2個Survivor區,其中Eden區占80%記憶體空間,每一塊Survivor區各占10%記憶體空間,比如說Eden區有800MB記憶體,每一塊Survivor區就100MB記憶體,如下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

平時可以使用的,就是Eden區和其中一塊Survivor區,那麼相當于就是有900MB的記憶體是可以使用的,如下圖所示。

年輕代和老年代分别适合什麼樣的垃圾回收算法

但是剛開始對象都是配置設定在Eden區内的,如果Eden區快滿了,此時就會觸發垃圾回收

此時就會把Eden區中的存活對象都一次性轉移到一塊空着的Survivor區。接着Eden區就會被清空,然後再次配置設定新對象到Eden區裡,然後就會如上圖所示,Eden區和一塊Survivor區裡是有對象的,其中Survivor區裡放的是上一次Minor GC後存活的對象。

如果下次再次Eden區滿,那麼再次觸發Minor GC,就會把Eden區和放着上一次Minor GC後存活對象的Survivor區内的存活對象,轉移到另外一塊Survivor區去。

是以這裡大家就能體會到,為啥是這麼配置設定記憶體空間了。因為之前分析了,每次垃圾回收可能存活下來的對象就1%,是以在設計的時候就留了一塊100MB的記憶體空間來存放垃圾回收後轉移過來的存活對象

比如Eden區+一塊Survivor區有900MB的記憶體空間都占滿了,但是垃圾回收之後,可能就10MB的對象是存活的。

此時就把那10MB的存活對象轉移到另外一塊Survivor區域就可以,然後再一次性把Eden區和之前使用的Survivor區裡的垃圾對象全部回收掉,如下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

接着新對象繼續配置設定在Eden區和另外那塊開始被使用的Survivor區,然後始終保持一塊Survivor區是空着的,就這樣一直循環使用這三塊記憶體區域。

這麼做最大的好處,就是隻有10%的記憶體空間是被閑置的,90%的記憶體都被使用上了

無論是垃圾回收的性能,記憶體碎片的控制,還是說記憶體使用的效率,都非常的好。

老年代

1.新生代裡的對象一般在什麼場景下會進入老年代

首先我們來看下面的圖,我們寫好的代碼在運作的過程中,就會不斷的建立各種各樣的對象,這些對象都會優先放到新生代的Eden區和Survivor1區。

年輕代和老年代分别适合什麼樣的垃圾回收算法

接着假如新生代的Eden區和Survivor1區都快滿了,此時就會觸發Minor GC,把存活對象轉移到Survivor2區去

年輕代和老年代分别适合什麼樣的垃圾回收算法

如下圖所示然後接着就會使用Eden區和Survivor2區,來配置設定新的對象,如下圖所示。

年輕代和老年代分别适合什麼樣的垃圾回收算法

這個過程上文已經講的非常的清楚了。那麼我們就來依次看看各種情況下,對象是如何進入老年代的,以及老年代的垃圾回收算法是什麼樣的?

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

按照上面的圖示的那個過程,其實大家可以了解為我們寫的系統剛啟動的時候,建立的各種各樣的對象,都是配置設定在新生代裡的。

然後慢慢系統跑着跑着,新生代就滿了,此時就會觸發Minor GC,可能就1%的少量存活對象轉移到空着的Survivor區中。

然後系統繼續運作,繼續在Eden區裡配置設定各種對象,大概就是這個過程。

那麼之前給大家講過,我們寫的系統中有些對象是長期存在的對象,他是不會輕易的被回收掉的,比如下面的代碼。

年輕代和老年代分别适合什麼樣的垃圾回收算法

隻要這個“Kafka”類還存在,那麼他的靜态變量“replicaManager”就會長期引用“ReplicaManager”對象,是以你無論新生代怎麼垃圾回收,類似這種對象都不會被回收掉的。

此時這類對象每次在新生代裡躲過一次GC被轉移到一塊Survivor區域中,此時他的年齡就會增長一歲

預設的設定下,當對象的年齡達到15歲的時候,也就是躲過15次GC的時候,他就會轉移到老年代裡去。

這個具體是多少歲進入老年代,可以通過JVM參數“-XX:MaxTenuringThreshold”來設定,預設是15歲,大家看下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

3.動态對象年齡判斷

這裡跟這個對象年齡有另外一個規則可以讓對象進入老年代,不用等待15次GC過後才可以。

他的大緻規則就是,假如說目前放對象的Survivor區域裡,一批對象的總大小大于了這塊Survivor區域的記憶體大小的50%,那麼此時大于等于這批對象年齡的對象,就可以直接進入老年代了。

說着有點抽象,具體還是看圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

假設這個圖裡的Survivor2區有兩個對象,這倆對象的年齡一樣,都是2歲然後倆對象加起來對象超過了50MB,超過了Survivor2區的100MB記憶體大小的一半了,這個時候,Survivor2區裡的大于等于2歲的對象,就要全部進入老年代裡去。

這就是所謂的動态年齡判斷的規則,這條規則也會讓一些新生代的對象進入老年代。

另外這裡要理清楚一個概念,就是實際這個規則運作的時候是如下的邏輯:年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n以上的對象都放入老年代。

其實說白了,無論是15歲的那個規則,還是動态年齡判斷的規則,都是希望那些可能是長期存活的對象,盡早進入老年代

既然你是長期存活的,那麼老年代才是屬于你的地盤,别賴在新生代裡占地方了。

4.大對象直接進入老年代

有一個JVM參數,就是“-XX:PretenureSizeThreshold”,可以把他的值設定為位元組數,比如“1048576”位元組,就是1MB。

他的意思就是,如果你要建立一個大于這個大小的對象,比如一個超大的數組,或者是别的啥東西,此時就直接把這個大對象放到老年代裡去。壓根兒不會經過新生代。

之是以這麼做,就是要避免新生代裡出現那種大對象,然後屢次躲過GC,還得把他在兩個Survivor區域裡來回複制多次之後才能進入老年代,那麼大的一個對象在記憶體裡來回複制,不是很耗費時間嗎?

是以說,這也是一個對象進入老年代的規則。

5.Minor GC後的對象太多無法放入Survivor區怎麼辦?

現在有一個比較大的問題,就是如果在Minor GC之後發現剩餘的存活對象太多了,沒辦法放入另外一塊Survivor區怎麼辦?如下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

比如上面這個圖,假設在發生GC的時候,發現Eden區裡超過150MB的存活對象,此時沒辦法放入Survivor區中,此時該怎麼辦呢?

這個時候就必須得把這些對象直接轉移到老年代去,如下圖所示。

年輕代和老年代分别适合什麼樣的垃圾回收算法

6.老年代空間配置設定擔保規則

這個時候大家又想提一個問題了,如果新生代裡有大量對象存活下來,确實是自己的Survivor區放不下了,必須轉移到老年代去,那麼如果老年代裡空間也不夠放這些對象呢?這該咋整呢?

别急,一步一圖,跟着下面的圖來看。

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

為啥檢查這個呢?因為最極端的情況下,可能新生代Minor GC過後,所有對象都存活下來了,那豈不是新生代所有對象全部要進入老年代?如下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

如果說發現老年代的記憶體大小是大于新生代所有對象的,此時就可以放心大膽的對新生代發起一次Minor GC了,因為即使Minor GC之後所有對象都存活,Survivor區放不下了,也可以轉移到老年代去。

但是假如執行Minor GC之前,發現老年代的可用記憶體已經小于了新生代的全部對象大小了

那麼這個時候是不是有可能在Minor GC之後新生代的對象全部存活下來,然後全部需要轉移到老年代去,但是老年代空間又不夠?

理論上,是有這種可能的。

是以假如Minor GC之前,發現老年代的可用記憶體已經小于了新生代的全部對象大小了,就會看一個“-XX:-

HandlePromotionFailure”的參數是否設定了

如果有這個參數,那麼就會繼續嘗試進行下一步判斷。

下一步判斷,就是看看老年代的記憶體大小,是否大于之前每一次Minor GC後進入老年代的對象的平均大小。

舉個例子,之前每次Minor GC後,平均都有10MB左右的對象會進入老年代,那麼此時老年代可用記憶體大于10MB。

這就說明,很可能這次Minor GC過後也是差不多10MB左右的對象會進入老年代,此時老年代空間是夠的,看下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法

如果上面那個步驟判斷失敗了,或者是“-XX:-HandlePromotionFailure”參數沒設定,此時就會直接觸發一次“Full

GC”,就是對老年代進行垃圾回收,盡量騰出來一些記憶體空間,然後再執行Minor GC。

如果上面兩個步驟都判斷成功了,那麼就是說可以冒點風險嘗試一下Minor GC。此時進行Minor GC有幾種可能。

第一種可能,Minor GC過後,剩餘的存活對象的大小,是小于Survivor區的大小的,那麼此時存活對象進入Survivor區域即可。

第二種可能,Minor GC過後,剩餘的存活對象的大小,是大于 Survivor區域的大小,但是是小于老年代可用記憶體大小的,此時就直接進入老年代即可。

第三種可能,很不幸,Minor GC過後,剩餘的存活對象的大小,大于了Survivor區域的大小,也大于了老年代可用記憶體的大小。此時老年代都放不下這些存活對象了,就會發生“Handle Promotion Failure”的情況,這個時候就會觸發一次“Full GC”。

Full GC就是對老年代進行垃圾回收,同時也一般會對新生代進行垃圾回收。

因為這個時候必須得把老年代裡的沒人引用的對象給回收掉,然後才可能讓Minor GC過後剩餘的存活對象進入老年代裡面。

如果要是Full GC過後,老年代還是沒有足夠的空間存放Minor GC過後的剩餘存活對象,那麼此時就會導緻所謂的“OOM”記憶體溢出了

因為記憶體實在是不夠了,你還是要不停的往裡面放對象,當然就崩潰了。

這段規則有點燒腦,但是我覺得如果大家仔細對這段文字多看兩遍,然後結合我們的圖,腦子裡想一想,基本都能看懂這個規則。

7.老年代垃圾回收算法

其實把上面的内容都看懂之後,大家現在基本就知道了Minor GC的觸發時機,然後就是Minor GC之前要對老年代空間大小做的檢查,包括檢查失敗的時候要提前觸發Full GC給老年代騰一些空間出來,或者是Minor GC過後剩餘對象太多放入老年代記憶體都不夠,也要觸發Full GC。包括這套規則,還有觸發老年代垃圾回收的Full GC時機,都給大家講清楚了。

簡單來說,一句話總結,對老年代觸發垃圾回收的時機,一般就是兩個:

要不然是在Minor GC之前,一通檢查發現很可能Minor GC之後要進入老年代的對象太多了,老年代放不下,此時需要提前觸發Full GC然後再帶着進行Minor GC;

要不然是在Minor GC之後,發現剩餘對象太多放入老年代都放不下了。

那麼對老年代進行垃圾回收采用的是什麼算法呢?

簡單來說,老年代采取的是标記整理算法,這個過程說起來比較簡單

大家看下圖,首先标記出來老年代目前存活的對象,這些對象可能是東一個西一個的。

年輕代和老年代分别适合什麼樣的垃圾回收算法

接着會讓這些存活對象在記憶體裡進行移動,把存活對象盡量都挪動到一邊去,讓存活對象緊湊的靠在一起,避免垃圾回收過後出現過多的記憶體碎片然後再一次性把垃圾對象都回收掉,大家看下圖。

年輕代和老年代分别适合什麼樣的垃圾回收算法