天天看點

Leetcode Linked List Cycle IIJVM記憶體配置設定和回收政策為什麼有兩塊survivor區域執行個體說明

我簡單介紹了下如何手動計算一個Java對象到底占用多少記憶體?今天就想聊下這個記憶體JVM到底是是如何配置設定和回收的。

Java整體來說還是一個GC比較友好的語言,無論是分代的垃圾收集,還是基于GC

Roots的可達性算法都是業界普遍的經典做法,關于Java的記憶體區域劃分以及GC的一些基本知識,我這裡就不贅述了,可以看我之前的部落格:

《深入了解Java虛拟機第2版》這本書非常值得一看,最近幾篇讀部落格都算這本書的讀書筆記吧。本人文筆很爛,是以都是記流水賬枯燥乏味的文章。進入正文之前還是要交代下環境:以下内容都是基于HotSpot虛拟機Server模式,垃圾收集器用的是預設的Serial和Serial

Old。

Leetcode Linked List Cycle IIJVM記憶體配置設定和回收政策為什麼有兩塊survivor區域執行個體說明

話說一圖勝千言,本也打算畫張活動圖就了事了:

但是畫完發現:一畫圖更麻煩,太大了看的累(想看的可以在建立視窗放大了看),其次感覺還是說不清楚(畫的不對的地方歡迎批評),最後覺得還是文字描述一下整個流程:

1、當JVM給一個對象配置設定記憶體的時候,如果啟動了本地線程配置設定緩存,将按線程優先在TLAB上分片,TLAB隻是起緩存作用減少高并發下CAS帶來的性能損失,跟GC的分代沒有沖突。

2、當配置設定一個對象的時候會優先在Eden區域配置設定,如果Eden有足夠的空間,那麼記憶體配置設定很順利的結束,不會觸發任何GC操作;

3、當Eden區域空間不足的時候,會嘗試着進行一次Minor GC,之是以說嘗試是因為在進行Minor

GC之前,虛拟機會檢查老年代最大可用的連續空間是否大于新生代所有對象空間總和,如果是的那麼可以保證這次Minor

GC順利進行;否則,虛拟機會檢查HandlePromotionFailure這個參數是否設定為允許擔保失敗,如果允許那麼虛拟機會根據經驗值(這個經驗值是曆次晉升到老年代對象的平均大小)來決定是否嘗試這次GC,如果小于或者JVM覺得不能冒險,那麼會進行一次Full

GC;

4、Minor

GC時會采用複制算法将所有存活的對象複制到Survivor空間中(既包括Eden區域存活的對象,也包括另外一個Survivor存活下來的對象),如果這時發現Survivor空間不足,那麼這些存活對象會直接進入老年代,這就是“空間配置設定”擔保,前面說到冒險,是因為老年代的空間仍有可能不夠,這時還是要進行一次Full

GC,但是除了極端情況,大部分時候通過擔保還是能有效避免頻繁的Full GC的,如果Full

GC後仍然沒有足夠空間,那隻能抛出OutOfMemoryError;

5、對象在Eden空間出生,經過第一次Minor

GC後能夠順利的被轉移到Survivor的話,那麼它的GC年齡就變成1,以後每在Survivor中熬過一次Minor

GC,年齡就增加1,直到超過一定程度(-XX:MaxTenuringThreshold,預設15歲)則晉升到老年代;

6、規則是死的,人是活的,虛拟機開發人員還想到了一個“動态對象年齡判定”算法:如果Survivor區域中相同年齡所有對象大小總和超過Survivor空間的一半,年齡大于等于該年齡的對象就可以直接進去老年代;

7、對象也可以直接配置設定在老年代,這主要是針對那些大對象,因為大對象的記憶體配置設定代價比較大(需要連續的記憶體空間),是以JVM提供了-XX:PretenureSizeThreshold這個參數。

我一開始很納悶HotSpot虛拟機為什麼要搞出兩個Survivor區域内,隻用一塊有何不妥嗎?最後在Stack Overflow找到一個答案:

 按照裡面的說法是為了減少虛拟機對記憶體碎片的處理,我想了半天我的了解是:

因為survivor中的對象在達到“老年”(-XX:MaxTenuringThreshold)之前肯定有對象已經變成“垃圾”了,這時候必須要對其進行回收,如果隻使用一個survivor的話,那麼要不容忍survivor存在記憶體碎片,要麼要對其進行記憶體整理,出于和對Eden區域同樣的考慮,是以實際上對Survivor的GC也是基于複制算法的,不過是從一個Survivor到另外一個Survivor(這也是GC日志中為什麼叫from

space和to

space),是以Survivor的兩個區是對稱的,沒有先後關系,是以Survivor區中可能同時存在從Eden複制過來對象,以及從前一個Survivor複制過來的對象,某一次GC結束時肯定會有一個Survivor是空的。

以上都是理論,下面結合一小段代碼簡單示範下上面的内容,這段代碼引自《深入了解Java虛拟機第2版》3.6.3節,我簡單展開說明下。為了友善解釋,我先把設定的虛拟機參數貼出來:

-XX:+UseSerialGC這裡使用預設的Serial GC進行說明;

-verbose:gc是為了列印出GC日志;

-Xms初始Java堆為20M;

-Xmx20M JVM最大使用的堆為20M不可擴充;

-Xmn10M,新生代的記憶體空間為10M;

-XX:SurvivorRatio=8 Eden與兩個Survivor的比例是8:1:1;

XX:PretenureSizeThreshold直接在老年代區域配置設定對象的門檻值為5M,其實可以不設定,這裡為了明确變量。

測試代碼如下:

Leetcode Linked List Cycle IIJVM記憶體配置設定和回收政策為什麼有兩塊survivor區域執行個體說明

我們一步步來看,首先隻執行48,49兩行:

Leetcode Linked List Cycle IIJVM記憶體配置設定和回收政策為什麼有兩塊survivor區域執行個體說明

通過GC日志發現沒有發生任何GC,Eden區域夠用(注意:關于一個Java對象導緻占用多大記憶體,參看。),接着執行51,52兩行:

可以看到了引發了一次GC,這是一次Minor

GC,同時可以看到使用的垃圾收集器是預設的(Def,關于GC日志的了解,可以參看《深入了解Java虛拟機第2版》一書)。引發GC的原因是Eden已經沒有足夠的記憶體容納allocation3對象,發生GC之後allocation2對象占用的記憶體空間被回收了,而allocation1“幸存”下來被轉移到了from

survivor區域。

接下來我們再執行57,58,59三行代碼:

Leetcode Linked List Cycle IIJVM記憶體配置設定和回收政策為什麼有兩塊survivor區域執行個體說明

第59行代碼引發了第二次GC,仍然是一次Minor

GC,可以看到allocation3和allocation4都被回收了,allocation5被順利的配置設定到了Eden空間,但是為什麼from

space變成了0%而老年代區域卻變成了6%,這6%應該是allocation1占用的,但是為什麼跑到老年代了呢?顯然它的“年齡”還沒有到15歲啊。

啊哈!還記得嗎JVM很聰明,它會“動态對象年齡判定”,從上一張圖可以看到,Survivor區域已經使用超過了50%(67%),而且顯然是同一年齡的對象(就一個對象嘛),是以在第二次GC的時候它晉升到了老年代,大家可以把allocation1對象配置設定為256kb再試試。

Ok,到目前為止都是Minor GC,想Stop-The-World很簡單,我們直接配置設定一個很大的對象試試:

Leetcode Linked List Cycle IIJVM記憶體配置設定和回收政策為什麼有兩塊survivor區域執行個體說明

不僅Full

GC了,而且記憶體溢出了,因為我們設定了-Xmx20M。同時可以看到JVM被逼急了在不同區域進行了GC,首先在新生代(Eden+Survivor0)将記憶體全部回收,導緻對象晉升到老年代,其次在老年代和“永久代(HotSpot)”也都進行了GC,但是一點收獲都沒有,最後隻能OOM。關于JVM的一些其他規則,比如大對象的配置設定,以及其他虛拟機的配置設定政策,就留給有興趣的同學自己試試了。

今天就寫到這,最後祝大家“六一快樂”……靠,看了下時間已經不是6.1,童年已經過去了!

PS:文章同步釋出在我的個人部落格