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下就可以。
優點:
- 不會長時間停
缺點:
- write_barrier略有開銷
- 上面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,過於保守。