GC算法 (標記清除、複製、標記整理、 分代收集) 、 新生代 老年代

 

一、標記-清除算法(Mark-Sweep)  

     1標記階段:首先通過根節點,標記所有從根節點開始的可達對象。未被標記的對象就是未被引用的垃圾對象

     2清除階段:清除所有未被標記的對象。

     

     不足:1效率問題:標記和清除兩個過程的效率都不高。

                2空間問題:標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要 分配較大對象時,無法找到足夠的連續內存而不得不提前出發另一次垃圾收集動作。

 

     標記-清除算法是現代垃圾回收算法的思想基礎。

 

 

二、複製算法(Copying)

     1將原有的內存空間分爲兩塊,每次只使用一塊,

     2在垃圾回收時,將正在使用的內存中的存活對象複製到未被使用的內存塊中,然後清除正在使用的內存塊中的所有對象。

     3交換兩個內存的角色,完成垃圾回收。

 

     與標記-清除算法相比,複製算法是一種相對高效的回收方法。

     不適用於存活對象較多的場合,如老年代。

 

 

三、標記—整理算法(Mark-Compact)

     1標記階段:先通過根節點,標記所有從根節點開始的可達對象,未被標記的爲垃圾對象

     2整理階段:將所有的存活對象壓縮到內存的一段,之後清理邊界外所有的空間

 

     標記-壓縮算法

     適合用於存活對象較多的場合,如老年代。

     它在標記-清除算法的基礎上做了一些優化。和標記-清除算法一樣,標記-壓縮算法也首先需要從根節點開始,對所有可達對象做一次標記。但之後,它並不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之後,清理邊界外所有的空間。

 

     標記壓縮對標記清除而言,有什麼優勢呢?

 

 

三種算法的比較:

      效率: 複製 > 標記整理 > 標記清除  (此處的效率只是簡單的對比時間複雜度,實際情況不一定如此)

      內存利用率: 標記整理 > 標記清除 > 複製

      內存整齊度: 複製 = 標記整理 > 標記清除

 

 

 

四、分代收集算法(Generational Collection)

     當前商業虛擬機的垃圾收集都採用“分代收集算法”,

     這種算法並沒有什麼新的思想,只是根據對象存活週期的不同將內存劃分爲幾塊。

     一般是將Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。

 

     在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用“複製算法”,只需要付出少量存活對象的複製成本就可以完成收集。

     在老年代中,因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。

 

 

五、新生代 與 老年代

      Java堆是JVM管理的最大一塊內存空間,主要存放對象實例。

      堆被分爲兩塊區域:新生代 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那樣頻繁。

 

六、新生代爲何需要兩個 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% 的空間浪費、複製對象的開銷等。

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章