Generational GC (Part one )



Generationanl GC

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

什么是分代垃圾回收

對象對的年齡

書上說:“人們 從眾多案例總結出一個經驗:‘大部分的對象再生成后馬上就變成了垃圾。很少有對象活的很久’。”,分代,引入年齡概念,經歷過一次GC的對象年齡為一歲。

新生代對象和老年對象

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

我們對新生代對象執行的GC稱為新生代GC(minor GC)。新生代GC的前提是大部分新生代對象都沒存活下來,GC在很短時間就結束了。新生代GC將存活了一定次數的對象當做老年代對象來處理。這時候我們需要把新生代對象上升為老年代對象(promotion)。老年代對象比較不容易成為垃圾,所以我們減少對其GC的頻率。我們稱面向老年代對象的GC為老年代GC(major GC)。

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

Ungar的分帶垃圾回收

堆的結構

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

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

此外我們准備出一個和堆不同的數組,稱為記錄集(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代是最好的。不過我想還是要看情況的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM