Generational GC (Part one )

Generationanl GC數組

引入年齡的概念,優先回收年輕的已成爲垃圾的對象。函數

什麼是分代垃圾回收

對象對的年齡

書上說:「人們 從衆多案例總結出一個經驗:‘大部分的對象再生成後立刻就變成了垃圾。不多有對象活的好久’。」,分代,引入年齡概念,經歷過一次GC的對象年齡爲一歲。操作系統

新生代對象和老年對象

分代垃圾回收中,將對象分爲幾類(幾代),針對不一樣的代使用不一樣的GC算法。剛生成的對象稱之爲新生代到達必定年齡的對象稱爲老年代對象3d

咱們對新生代對象執行的GC稱爲新生代GC(minor GC)。新生代GC的前提是大部分新生代對象都沒存活下來,GC在很短期就結束了。新生代GC將存活了必定次數的對象當作老年代對象來處理。這時候咱們須要把新生代對象上升爲老年代對象(promotion)。老年代對象比較不容易成爲垃圾,因此咱們減小對其GC的頻率。咱們稱面向老年代對象的GC爲老年代GC(major GC)。指針

分代垃圾回收是將多種垃圾回收算法並用的一種垃圾回收機制。code

Ungar的分帶垃圾回收

堆的結構

Ungar分代垃圾回收中,堆結構圖以下所示。總共須要四個空間,分別是生成空間、兩個大小相等的倖存空間、老年代空間,分別用$new_start、$survivor1_start、$survivor2_start、$old_start這四個變量指向他們開頭。對象

生成空間和幸運空間合稱爲新生代空間,新生代對象會被分配到新生代空間,老年代對象則會被分配到老年代空間裏。Ungar 在論文裏把生成空間、倖存空間以及老年代空間的大小分別設成了 140K 字節、28K 字節和 940K 字節。blog

此外咱們準備出一個和堆不一樣的數組,稱爲記錄集(remembered set),設爲 $rs。遞歸

  • 生成空間,是生成對象的空間。當空間滿了新生代GC就會啓動,將生成空間裏的對象複製,與GC複製算法同樣。
  • 兩個倖存空間,一個From一個To。
  • 新生代GC將From空間和生成對象空間裏活動的對象複製到To空間中。(這有一個問題,會形成To可能不夠用)
  • 只有通過必定次數的新生代GC才能被放到老年代空間中去。

過程以下圖所示:

  • 新生代GC要注意一點,就是老年代空間到新生代空間的引用。所以除了通常GC的根,老年代空間裏也會有新生代空間對象的引用來當作根。

  • 分代垃圾回收的優勢,將重點放置新生代的對象,他們容易被回收。這樣會縮減GC所需的時間。可是,若是咱們讓老年代對象引用新生代對象這樣一來等同於全部對象都從根引用。這樣就沒有這樣的優點了。
  • 因此咱們引入記錄集。記錄集用來記錄老年代對象到新生代對象的引用。這樣就能夠不搜索老年代空間裏的全部對象,而是經過搜索記錄集來發現老年代對象到新生代對象的引用關。
  • 當老年代空間滿了的時候,就要進行老年代GC了。

記錄集

記錄集用於高效的尋找從老年代對象到新生代對象的引用。在新生代 GC 時將記錄集當作根(像根同樣的東西),並進行搜索,以發現指向新生代空間的指針。

不過若是咱們爲此記錄了引用的目標對象(即新生代對象),那麼在對這個對象進行晉升(老 年化)操做時,就無法改寫所引用對象(即老年代對象)的指針了。以下圖示:

經過查找可知對象A時新生代GC的對象,執行GC後它升級爲了老年代對象A'。但在這個狀態下咱們不發更新B的引用爲A',記錄集裏沒有存儲老年代對象 B 引用了新生代對象 A的信息。

因此記錄集裏記錄的不是新生代對象,而是老年代對象。他記錄的老年代對象都是有子對象是新生代對象的。這樣咱們就能去更新B了。

記錄集大部分使用固定大小數組來實現。那麼咱們如何向記錄集裏插入對象呢?關於寫入屏障內容。

寫入屏障

將老年代對象記錄到記錄集裏,咱們利用寫入屏障(write barrier)。write_barrier()函數。

write_barrier(obj, field, new_obj){
    if(obj >= $old_start && new_obj < $old_start && obj.remembered == FALSE)
        $rs[$rs_index] = obj
        $rs_index++
        obj.remembered = TRUE
    *field = new_obj
    
}
  • obj 是發出引用的對象,obj內存放要更新的指針,而field指的就是obj內的域,new_obj 是在指針更新後成爲引用的目標對象。

  • 檢測發出引用的對象是否是老年代對象,指針更新後引用的目標是否是新生代對象,發出引用的對象是否尚未被記錄到記錄集中。
  • 當這些都爲真時,obj就被記錄到記錄集中了。
  • $rs_index適用於新紀錄對象的索引
  • 最後一行,用於更新指針。

對象的結構

對象的頭部除了包含對象的種類和大小以外,還有三條信息,分別是對象的年齡(age)、已經複製完成的標識(forwarded)、向記錄集中記錄完畢的標識(remembered)。

  • age標識新生代對象存活的次數。超過必定次數,就會被當作老年代對象。
  • forwarded,用來防止重複複製相同的對象。
  • remembered用來防止登記相同的對象。不過remembered只適用於老年代對象,age和forwarded只使用新生代的對象。
  • 除上面三點以外,這裏也是用forwarding指針以前的垃圾回收同樣。在forwarding指針中利用obj.field1,用obj.forwarding訪問obj.field1。

對象結構以下圖示:

分配

在生成空間裏進行,執行new_obj()函數代碼以下:

new_obj(size){
    if($new_free + size >= $survivor1_start)
        minor_gc()
        if($new_free + size >= $survivor1_start)
            allocation_fail()
    
    obj = $new_free
    $new_free += size
    obj.age = 0
    obj.forwarded = FALSE
    obj.remembered = FALSE
    obj.size = size
    return obj
    
}
  • $new_free指向生成空間的開頭
  • 檢測生成空間是否存在size大小的分塊。若是沒有就執行新生代GC。執行後全部對象都到倖存空間去了,生成空間絕對夠用。
  • 分配空間。
  • 對對象進行一系列的標籤之類的設置(初始化)。而後返回。

新生代GC

生成空間被對象沾滿後,新生代GC就會啓動。minor_gc()函數負責吧生成空間 和From空間的活動對象移動到To空間。

咱們先來了解minor_gc()中進行復制對象的函數copy()。

copy(obj){
    if(obj.forwarded == FALSE) // 檢測對象是否複製完畢
        if(obj.age < AGE_MAX)  //  沒有複製則檢查對象年齡
            copy_data($to_survivor_free, obj, obj.size)// 開始複製對象操做
            obj.forwarede = TRUE
            obj.forwarding = $to_survivor_free
            $to_survivor_free.age++
            $to_survivor_free += obj.size// 複製對象結束
            for(child :children(obj)) // 遞歸複製其子對象
                *child = copy(*child)
        else
            promote(obj) //若是年齡夠了,則進行晉級的操做,升級爲老年代對象。
    return obj.forwarding  //返回索引
}
promote(obj){
    new_obj = allocate_in_old(obj)
    if(new_obj == NULL) // 判斷可否將obj放入老年代空間中。
        major_gc() //不能去就啓動gc
        new_obj = allocate_in_old(obj)// 再次查詢 
        if(new_obj == NULL) //再次查詢。
         allocation_fail()//不能放入的話就報錯啦。
    obj.forwarding = new_obj // 能放入則設置對象屬性
    obj.forwarded = TRUE
        
    for(child :children(new_obj)) //啓動GC
        if(*child < $old_start) // obj是否有指向新生代對象的指針
            $rs[$rs_index] = new_obj // 若是有就將obj寫到記錄集裏。
            $rs_index++
            new_obj.remembered = TRUE
            return
}
minor_gc(){
    $to_survivor_free = $to_survivor_start // To空間開頭
    for(r :$roots) // 尋找能從跟複製的新生代對象
        if(*r <$old_start)
            *r = copy(*r)
    i = 0 // 開始搜索記錄集中的對象$rs[i] 執行子對象的複製操做。
    while(i<$rs_index)
        has_new_obj = FALSE
        for(child :children($rs[i]))
            if(*child <$old_start)
                *child = copy(*child)
                if(*child < $old_start) //檢查複製後的對象在老年代空間仍是心神的古代空間 
                    has_new_obj = TRUE  //若是在新生代空間就設置爲False不然True 
        if(has_new_obj ==FALSE) // 若是爲False,$rs[i]就沒有指向新生代空間的引用。接下來就要本身在記錄集裏的信息了。
            $rs[i].remembered = FALSE
            $rs_index--
            swap($rs[i], $rs[$rs_index])
        else
            i++
    swap($from_survivor_start, $to_survivor_start) // From 和To互換空間
    
}

倖存空間沾滿了怎麼辦?

  • 一般的GC複製算法把空間二等分爲From空間和To空間,即便From空間裏的對象都還 活着,也確保能把它們收納到To空間裏去。
  • 不過在Ungar的分代垃圾回收裏,To倖存空間必 須收納 From 倖存空間以及生成空間中的活動對象。From 倖存空間和生存空間的點大小比 To 幸 存空間大,因此若是活動對象不少,To 倖存空間就沒法容納下它們。
  • 當發生這種狀況時,穩妥起見只能把老年代空間做爲複製的目標空間。固然,若是頻繁發生 這種狀況,分代垃圾回收的優勢就會淡化。
  • 然而實際上經歷晉升的對象不多,因此這不會有什麼重大問題,所以在僞代碼中咱們就把這 步操做省略掉了。

老年代GC

就以前介紹的GC都行,可是具體使用哪一個看想要的效果以及內存的大小來決定。通常來講GC標記清除就挺好的。

優缺點

吞吐量獲得改善

經過使用分代垃圾回收,能夠改善 GC 所花費的時間(吞吐量)。正如 Ungar 所說的那樣:「據實驗代表,分代垃圾回收花費的時間是 GC 複製算法的 1/4。」可見分代垃圾 回收的導入很是明顯地改善了吞吐量。

在部分程序中會起到副作用

「不少對象年紀輕輕就會死」這個法則畢竟只適合大多數狀況,並不適用於全部程序。固然, 對象會活得好久的程序也有不少。對這樣的程序執行分代垃圾回收,就會產生如下兩個問題。

  • 新生代GC花費時間增多
  • 老年代GC頻繁

除此以外,寫入屏障等也致使了額外的負擔,下降了吞吐量。當新生代GC帶來的速度提高特別小的時候,這樣作很明顯是會形成相反的效果。

記錄各代之間的引用的方法

Ungar的分帶垃圾回收,使用記錄集來記錄各個代間的引用關係。這樣每一個發出引用的對象就要花費1個字的空間。此外若是各代之間引用超級多還會出現記錄集溢出的問題。(前面說過記錄集通常是一個數組。)

卡片標記

Paul R.Wilson 和 Thomas G.Moher開發的一種叫作卡片標記(card marking)的方法。

首先把老年代空間按照等大分割開來。每個空間就成爲卡片,聽說卡片適合大小時128字節。另外還要對各個卡片準備一個標誌位,並將這個做爲標記表格(mark table)進行管理。

當由於改寫指針而產生從老年對象到新生代對象的引用時,要事前對被寫的域所屬的卡片設置標誌位,及時對象誇兩張卡片,也不會有什麼影響。

GC時會尋找位圖表格,當找到了設置了標誌位的卡片時,就會從卡片的頭開始尋找指向新生代空間的引用。這就是卡片的標記。

由於每一個卡片只須要一個位來進行標記,因此整個位表也只是老年代空間的千分之一,此外不會出現溢出的狀況。可是可能會出現搜索卡片上花費大量時間。所以只有在局部存在的老年代空間指向新生代空間的引用時卡片標記才能發揮做用。

頁面標記

許多操做系統以頁面爲單位管理內存空間,若是在卡片標記中將卡片和頁面設置爲一樣大小,就可使用OS自帶的頁了。

一旦mutator對堆內的某一個頁面進行寫入操做,OS就會設置根這個也面對應的位,咱們把這個位叫作重寫標誌位(dirty bit)。

卡片標記是搜索標記表格,而頁面標記(page marking)則是搜索這個頁面重寫標誌位。

根據 CPU 的不一樣,頁面大小也不一樣,不過咱們通常採用的大小爲4K字節。這個方法只適用於能利用頁面重寫標誌位或能利用內存保護功能的環境。

多代垃圾回收

Multi-generational GC

將對象劃分爲多個代,這樣一來能晉升的對象就會一層一層的減小了。

  • 除了最老的那一代,每一代都有一個記錄集。X代的記錄集只記錄來自比X老的其餘代的引用。
  • 分代越多,無心對象越快被回收,這個方法每一層的對象都在減小。
  • 可是不能過分增長,想一想一下,咱們的cpu居然同時在作不少的GC算法,簡直不能理解是吧。
  • 書上說,2-3代是最好的。不過我想仍是要看狀況的。
相關文章
相關標籤/搜索