垃圾回收算法(6)三色標記


GC目前的問題是,會暫停、阻礙代碼的運行,即stop the world。增量式GC處理的就是這個問題。將GC變得可一階段一階段進行。
 
分階段運行的思路並不難,但具體要解決的問題其實是 分階段GC后,如何保證下次繼續時,中斷過程中引用關系的變化不會對GC造成影響
 
三色標記法是一個邏輯上的抽象,將對象分成 白:未搜索,灰:正搜索,黑:已搜索。
 
在這里,和前面引用計數中提到的標色不一樣,這里只是一個邏輯概念,在實現中並沒有所謂的black, white。
 
mark_sweep按增量來排,可以分成三個階段: 根查找、標記、清除
 
incremental_gc() {
  case $gc_phase
    if GC_ROOT_SCAN
      root_scan_phase()
    if GC_MARK
      incremental_mark_phase()
    else
      incremental_sweep_phase ()
}
 
root_scan_phase() {
  for r : $root
    mark(r)
  $gc_phase = GC_MARK
}
 
mark(obj) {
  if !obj.mark
    obj.mark = true
    push(obj, $mark_stack)         // 理解下,不分段的GC中,由於是用遞歸方式直接深度搜索到底,所以不需要這個stack,而這個搜索過程目前會中斷了,因此需要這樣一個數據結構來記錄。
}

 

 
上面這mark,就邏輯上把根對象由白標記為灰了。
 
incremental_mark_phase() {
  for i : range 1..MARK_MAX          // 有個值,每次就處理這么多,可以有效防止stop the world
    if !is_empty($mark_stack)          // 以下棧中有值就取,無值就掃root
      obj = pop($mark_stack)
      for child : children(obj)
        mark(child)
    else
      for r : $root
        mark(r)
      while !is_empty($mark_stack) 
        obj = pop($mark_stack)
        for child : children(obj)
          mark(child)
 
  $gc_phase = GC_SWEEP            // 直接進入下階段
  $sweeping = $head_start
  return
}
 
// 清除就不說了,同樣思路,設置個最大值,每次只處理這么多。因為是mark_sweep,所以只要將未標記的引入free_list即可!!!!
 
到這里遇到了關鍵問題:如果在垃圾回收階段中間有新的對象引入,或是由於對象的指向關系,使得原本應該mark到的活動對象漏掉了,怎么辦?這里會出現因為此對象沒有mark而被清除的問題。
新對象加入好說,對象的指向變化導致沒有mark到,是這種情況:
上圖,C原先是應該被B遞歸搜索標記的。但在GC休息時,B不再指向C,C反而被A指向了。這個C在本輪就會被回收掉。
這個問題是三色與mark之間的對應關系沒有對應好導致。
現在入mark_stack棧且mark與灰對應,搜索完成后,mark的是黑。而垃圾回收的依據,是mark過的對象,黑。而白,一定是非mark過,一定會被回收,但這里,白不應該被回收。因此,這個C對象的白色是錯誤的,要處理。
 
wirte_barrier(obj, field, newobj) {
  if newobj.mark == FALSE            
    newobj.mark = true                    // 這里,因為本身write_barrier是一個賦值操作,因此此對象天生就被mark也算正常
    push(newobj, $mark_stack)         // 這個動作,就強行標記為灰了
 
  field = newobj
}
處理后,新引用的對象也是mark狀態,是這樣的:
 
最后,如果新分配對象時,mark階段已經完了,正在sweep,怎么處理?很簡單,只要判斷分配的對象在sweeping指針的前面還是后面。如果在前面已經sweep過的區域,直接忽略;如果在后面,簡單mark下就可以。
 
優點:
  1. 不會長時間停
 
缺點:
  1. write_barrier略有開銷
  2. 上面write_barrier會將對象強行制灰,也就是強行標記,是不大精確的,會造成當前輪次的垃圾殘留。
 
針對缺點2:
場景是,write_barrier后,是對的。但再次回頭,比如A又指向B了,那C這個垃圾在本輪就發現不了。
 
改良型(steele)的write_barrier
mark(obj) {
  if !obj.mark
    push(obj, $mark_stack)   // 和上面對表,少了mark = true
}
 
// 上面減少了mark的工作,將mark穩定到出棧處。這樣可以引出下面的write_barrier
// 這里,灰色已經不再是mark過,而是入過棧。反而,黑色才是mark過。
write_barrier(obj, filed, newobj) {
  if $gc_phase == GC_MARK && obj.mark && !newobj.mark      // 邏輯也很清晰,不再一棍子將新加入的認為是非垃圾,而是認為“需要check是否垃圾”。如何check,就是將引用它的對象回滾成灰。
    obj.mark = false
    push(obj, $mark_stack)
 
  field = newobj
}

即:

 
還有基於快照思想的一種write_barrier的思路:
在write_barrier中入mark_stack棧的不是新對象,而是舊對象!這樣,對於之前的對象的引用仍然存在,就不會丟對象。那么mark階段中新生成的對象怎么處理?它直接將其mark,過於保守。
 
 
 
 


免責聲明!

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



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