天天看點

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

作者:一個即将被退役的碼農

參考文檔:

深入了解JVM虛拟機——JVM運作時棧結構和方法調用

深入了解JVM虛拟機——JVM是怎麼實作invokedynamic的

深入了解JVM虛拟機——JVM 垃圾回收機制及其實作原理

深入了解JVM虛拟機——類的加載機制

深入了解JVM虛拟機——Java記憶體模型原理

目錄:

躲過15次GC之後進入老年代動态對象年齡判斷

大對象直接進入老年代

Minor GC後的對象太多,無法放入Survivor區怎麼辦? 老年代空間配置設定擔保規則

老年代垃圾回收算法思考題

1、回顧

本文要給大家說說,新生代裡的對象一般在什麼場景下會進入老年代。

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

如下圖所示

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

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

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

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

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

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

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

3、動态對象年齡判斷

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

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

假設這個圖裡的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區怎麼辦?如下圖。

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

這個時候大家又想提一個問題了,如果新生代裡有大量對象存活下來,确實是自己的Survivor區放不下了,必須轉移到老年代去

那麼如果老年代裡空間也不夠放這些對象呢?這該咋整呢?

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

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

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

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

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

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

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

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

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

如果上面那個步驟判斷失敗了,或者是“-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之後,發現剩餘對象太多放入老年代都放不下了。

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

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

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

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

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

然後再一次性把垃圾對象都回收掉,大家看下圖。

深入了解JVM虛拟機——年輕代和老年代分别适合的垃圾回收算法

大家一定要注意一點,這個老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。

如果系統頻繁出現老年代的Full GC垃圾回收,會導緻系統性能被嚴重影響,出現頻繁卡頓的情況。

是以後面用各種案例給大家展現出來的,就是在各種業務系統的生産故障下,怎麼去一步一步分析到底為什麼頻繁的Full GC,然後怎麼來調整JVM的各種參數進行優化。

其實大家如果透徹了解了最近的幾篇文章涵蓋的JVM的運作原理,就會知道,所謂JVM優化,就是盡可能讓對象都在新生代裡配置設定和回收,盡量别讓太多對象頻繁進入老年代,避免頻繁對老年代進行垃圾回收,同時給系統充足的記憶體大小,避免新生代頻繁的進行垃圾回收。

關于如何優化JVM,後續會有大量的案例帶着大家去實戰,而且會給出模拟生産的代碼,讓大家運作起來看到模拟出來的案發現場是如何導緻JVM頻繁GC的,對性能是如何影響的,然後再一步一步來優化JVM參數解決性能問題。

8、思考題

大家可以借助上面的方法估算一下系統對記憶體使用壓力,每秒鐘系統會使用多少記憶體空間,然後多長時間會觸發一次垃圾回收,垃圾回收之後,你們系統内大體會有多少對象存活下來?為什麼?都有哪些對象會存活下來? 存活下來的對象會占多少記憶體空間?

觸發Minor GC之前會如何檢查老年代大小,涉及哪幾個步驟和條件? 什麼時候在Minor GC之前就會提前觸發一次Full GC?

Full GC的算法是什麼?

Minor GC過後可能對應哪幾種情況?

哪些情況下Minor GC後的對象會進入老年代?

這些問題會在後面的文章中一個個解答,關注不後悔!

繼續閱讀