1. 程式人生 > >JAVA分代收集機制詳解

JAVA分代收集機制詳解

當前 管理 目的 但是 ostc 之間 ring 等價 垃圾

Java堆中是JVM管理的最大一塊內存空間。主要存放對象實例。 在JAVA中堆被分為兩塊區域:新生代(young)、老年代(old)。 堆大小=新生代+老年代;(新生代占堆空間的1/3、老年代占堆空間2/3) 新生代又被分為了eden、from survivor、to survivor(8:1:1); 新生代這樣劃分是為了更好的管理堆內存中的對象,方便GC算法---復制算法來進行垃圾回收。 JVM每次只會使用eden和其中一塊survivor來為對象服務,所以無論什麽時候,都會有一塊survivor空間,因此新生代實際可用空間只有90%。 新生代GC(minor gc)----------指發生在新生代的垃圾回收動作,因為JAVA對象大多數都是朝生夕死的特性,所以minor gc非常平凡,使用復制算法快速的回收。 新生代幾乎是所有JAVA對象出生的地方,JAVA對象申請的內存和存放都是在這個地方。 當對象在eden(其中包括一個survivor,假如是from),當此對象經過一次minor gc後仍然存活,並且能夠被另外一塊survivor所容納(這裏survivor則是to了),則使用復制算法將這些仍然存活的對象復制到to survior區域中,然後清理掉eden和from survivor區域,並將這些存活的對象年齡+1,以後對象在survivor中每熬過一次gc則增加1,當年齡達到某個值時(默認15,通過設置參數-xx:maxtenuringThreshold來設置),這些對象就會成為老年代! 但是也不一定,當一些較大的對象(需要分配連續的內存空間)則直接進入老年代。 老年代GC(major gc)----------指發生在老年代的垃圾回收動作,所采用是的標記--整理算法。 老年代幾乎都是經過survivor熬過來的,它們是不會那麽容易“死掉”,因此major gc不會想minor gc那樣頻繁。

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% 的空間浪費、復制對象的開銷等。

分類: JVM

JAVA分代收集機制詳解