目錄算法
Generationanl GC數組
引入年齡的概念,優先回收年輕的已成爲垃圾的對象。函數
書上說:「人們 從衆多案例總結出一個經驗:‘大部分的對象再生成後立刻就變成了垃圾。不多有對象活的好久’。」,分代,引入年齡概念,經歷過一次GC的對象年齡爲一歲。操作系統
分代垃圾回收中,將對象分爲幾類(幾代),針對不一樣的代使用不一樣的GC算法。剛生成的對象稱之爲新生代,到達必定年齡的對象稱爲老年代對象。3d
咱們對新生代對象執行的GC稱爲新生代GC(minor GC)。新生代GC的前提是大部分新生代對象都沒存活下來,GC在很短期就結束了。新生代GC將存活了必定次數的對象當作老年代對象來處理。這時候咱們須要把新生代對象上升爲老年代對象(promotion)。老年代對象比較不容易成爲垃圾,因此咱們減小對其GC的頻率。咱們稱面向老年代對象的GC爲老年代GC(major GC)。指針
分代垃圾回收是將多種垃圾回收算法並用的一種垃圾回收機制。code
Ungar分代垃圾回收中,堆結構圖以下所示。總共須要四個空間,分別是生成空間、兩個大小相等的倖存空間、老年代空間,分別用$new_start、$survivor1_start、$survivor2_start、$old_start這四個變量指向他們開頭。對象
生成空間和幸運空間合稱爲新生代空間,新生代對象會被分配到新生代空間,老年代對象則會被分配到老年代空間裏。Ungar 在論文裏把生成空間、倖存空間以及老年代空間的大小分別設成了 140K 字節、28K 字節和 940K 字節。blog
此外咱們準備出一個和堆不一樣的數組,稱爲記錄集(remembered set),設爲 $rs。遞歸
過程以下圖所示:
記錄集用於高效的尋找從老年代對象到新生代對象的引用。在新生代 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 是在指針更新後成爲引用的目標對象。
最後一行,用於更新指針。
對象的頭部除了包含對象的種類和大小以外,還有三條信息,分別是對象的年齡(age)、已經複製完成的標識(forwarded)、向記錄集中記錄完畢的標識(remembered)。
對象結構以下圖示:
在生成空間裏進行,執行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 }
生成空間被對象沾滿後,新生代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都行,可是具體使用哪一個看想要的效果以及內存的大小來決定。通常來講GC標記清除就挺好的。
經過使用分代垃圾回收,能夠改善 GC 所花費的時間(吞吐量)。正如 Ungar 所說的那樣:「據實驗代表,分代垃圾回收花費的時間是 GC 複製算法的 1/4。」可見分代垃圾 回收的導入很是明顯地改善了吞吐量。
「不少對象年紀輕輕就會死」這個法則畢竟只適合大多數狀況,並不適用於全部程序。固然, 對象會活得好久的程序也有不少。對這樣的程序執行分代垃圾回收,就會產生如下兩個問題。
除此以外,寫入屏障等也致使了額外的負擔,下降了吞吐量。當新生代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
將對象劃分爲多個代,這樣一來能晉升的對象就會一層一層的減小了。