JAVA分代收集機制詳解
JVM 新生代為何需要兩個 Survivor 空間?
我們知道,目前主流的虛擬機實現都采用了分代收集的思想,把整個堆區劃分為新生代和老年代;新生代又被劃分成 Eden 空間、 From Survivor 和 To Survivor 三塊區域。
看書的時候有個疑問,為什麽非得是兩個 Survivor 空間呢?要回答這個問題,其實等價於:為什麽不是0個或1個 Survivor 空間?為什麽2個 Survivor 空間可以達到要求?
為什麽不是0個 Survivor 空間?
這個問題等價於:為什麽需要 Survivor 空間。我們看看如果沒有 Survivor 空間的話,垃圾收集將會怎樣進行:一遍新生代 gc 過後,不管三七二十一,活著的對象全部進入老年代,即便它在接下來的幾次 gc 過程中極有可能被回收掉。這樣的話老年代很快被填滿, Full GC 的頻率大大增加。我們知道,老年代一般都會被規劃成比新生代大很多,對它進行垃圾收集會消耗比較長的時間;如果收集的頻率又很快的話,那就更糟糕了。基於這種考慮,虛擬機引進了“幸存區”的概念:如果對象在某次新生代 gc 之後任然存活,讓它暫時進入幸存區;以後每熬過一次 gc ,讓對象的年齡+1,直到其年齡達到某個設定的值(比如15歲), JVM 認為它很有可能是個“老不死的”對象,再呆在幸存區沒有必要(而且老是在兩個幸存區之間反復地復制也需要消耗資源),才會把它轉移到老年代。
總之,設置 Survivor 空間的目的是讓那些中等壽命的對象盡量在 Minor GC 時被幹掉,最終在總體上減少虛擬機的垃圾收集過程對用戶程序的影響。
為什麽不是1個 Survivor 空間?
回答這個問題有一個前提,就是新生代一般都采用復制算法進行垃圾收集。原始的復制算法是把一塊內存一分為二, gc 時把存活的對象(Eden和Survivor to)從一塊空間(From space)復制到另外一塊空間(To space),再把原先的那塊內存(From space)清理幹凈,最後調換 From space 和 To space 的邏輯角色(這樣下一次 gc 的時候還可以按這樣的方式進行)。
我們知道,在 HotSpot 虛擬機裏, Eden 空間和 Survivor 空間默認的比例是 8:1 。我們來看看在只有一個 Survivor 空間的情況下,這個 8:1 會有什麽問題。此處為了方便說明,我們假設新生代一共為 9 MB 。對象優先在 Eden 區分配,當 Eden 空間滿 8 MB 時,觸發第一次 Minor GC 。比如說有 0.5 MB 的對象存活,那這 0.5 MB 的對象將由 Eden 區向 Survivor 區復制。這次 Minor GC 過後, Eden 區被清理幹凈, Survivor 區被占用了 0.5 MB ,還剩 0.5 MB 。到這裏一切都很美好,但問題馬上就來了:從現在開始所有對象將會在這剩下的 0.5 MB 的空間上被分配,很快就會發現空間不足,於是只好觸發下一次 Minor GC 。可以看出在這種情況下,當 Survivor 空間作為對象“出生地”的時候,很容易觸發 Minor GC ,這種 8:1 的不對稱分配不但沒能在總體上降低 Minor GC 的頻率,還會把 gc 的時間間隔搞得很不平均。把 Eden : Survivor 設成 1 : 1 也一樣,每當對象總大小滿 5 MB 的時候都必須觸發一次 Minor GC ,唯一的變化是 gc 的時間間隔相對平均了。
上面的論述都是以“新生代使用復制算法”這個既定事實作為前提來討論的。如果不是這樣,比如說新生代采用“標記-清除”或者“標記-整理”算法來實現幸存對象的移動,好像確實是只需要一個 Survivor 就夠了。至於主流的虛擬機實現為什麽不考慮采用這種方式,我也不是很清楚,或許有實現難度、內存碎片或者執行效率方面的考慮吧。
為什麽2個 Survivor 空間可以達到要求?
問題很清楚了,無論 Eden 和 Survivor 的比例怎麽設置,在只有一個 Survivor 的情況下,總體上看在新生代空間滿一半的時候就會觸發一次 Minor GC 。那有沒有提升的空間呢?比如說永遠在新生代空間滿 80% 的時候才觸發 Minor GC ?
事實上是可以做到的:我們可以設兩個 Survivor 空間( From Survivor 和 To Survivor )。比如,我們把 Eden : From Survivor : To Survivor 空間大小設成 8 : 1 : 1 ,對象總是在 Eden 區出生, From Survivor 保存當前的幸存對象, To Survivor 為空。一次 gc 發生後:
1)Eden 區活著的對象 + From Survivor 存儲的對象被復制到 To Survivor ;
2) 清空 Eden 和 From Survivor ;
3) 顛倒 From Survivor 和 To Survivor 的邏輯關系: From 變 To , To 變 From 。
可以看出,只有在 Eden 空間快滿的時候才會觸發 Minor GC 。而 Eden 空間占新生代的絕大部分,所以 Minor GC 的頻率得以降低。當然,使用兩個 Survivor 這種方式我們也付出了一定的代價,如 10% 的空間浪費、復制對象的開銷等。
JAVA分代收集機制詳解