年輕代
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之后,發現剩余對象太多放入老年代都放不下了。
那么對老年代進行垃圾回收采用的是什么算法呢?
簡單來說,老年代采取的是標記整理算法,這個過程說起來比較簡單
大家看下圖,首先標記出來老年代當前存活的對象,這些對象可能是東一個西一個的。

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

大家一定要注意一點,這個老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。
如果系統頻繁出現老年代的Full GC垃圾回收,會導致系統性能被嚴重影響,出現頻繁卡頓的情況。