參考書籍
1.垃圾回收算法手冊 自動內存管理的藝術
2.垃圾回收的算法與實現
標記清掃回收
垃圾回收算法一般都是現在的自動內存管理系統中都會使用到的,例如python、go等語言都實現了自己的垃圾回收機制,從而解放了用戶手動來管理內存的問題。一個自動內存管理系統一般都分爲主要的三個操作:1.爲新對象分配空間,2.確實已分配空間的對象是否存活,3.回收死亡對象佔用的內存空間。通過這三種方式,來在程序運行的過程中,保持程序能夠對資源有個比較良好的利用率,少佔用不必要的內存,避免內存泄露等問題。標記清掃回收也只是其中的一種算法。
在實際的程序的運行過程中,如果在單線程的運行過程中,內存的申請過程和回收過程都在一個線程中完成不會有競爭的問題,但是如果在多線程的運行過程中,內存的申請過程與回收過程都可以在不同的線程中來執行,這樣就會導致在回收的過程中有比較複雜的策略來保證內存申請過程、內存的回收過程的正確性。當前比較有名的STW(stop the world)的方式就是指,申請過程可以在多個不同的線程上運行,但只有一個回收過程線程,當進行回收過程的時候,申請過程所在的線程就停止運行,這樣簡化了整個內存管理的實現,比如早起版本的go語言就通過這個模型來實現的內存管理系統。
標記清掃回收的過程
標記清掃的過程主要分爲三步,分別爲分配、標記和清掃。
分配
如果在線程無法分配對象的時候,喚起回收器回收空餘的內存,然後再次嘗試申請內存分配對象,如果在回收完成之後仍然沒有足夠的內存來滿足分配需求,則說明內存已經沒有多餘可用,內存爆滿會引發一些異常的情況。相關的僞代碼可參考如下(借鑑自書中僞代碼):
New():
ref <- allocate()
if ref == null // 如果申請的內存爲空 則證明沒有內存可使用需要回收一下
collect() // 收集回收
ref <- allocate() // 再次申請內存
if ref == null // 此時說明沒有更多的內存可以申請,內存爆滿
error "Out of Memory"
return ref
atomic collect(): // 回收過程標記在線程中是個原子操作
markFromRoots() // 從根部查找對分配的對象做標記
sweep(HeapStart, HeapEnd) // 回收釋放的對象
#####標記
標記主要就是將當前仍在使用的對象標記爲可用對象,搜索出那些沒有進行標記的對象從而將該對象刪除釋放內存。
markFromRoots():
initialise(worklist) // 初始化一個空的列表保存標記後的對象
for each fld in Roots // 通過根遍歷所有的子節點
ref <- *fld // 獲取引用的對象
if ref != null && not isMarked(ref) // 如果不爲空並且沒有被標記
setMarked(ref) // 設置標記
add(worklist, ref) // 添加到worklist列表中
mark() // 循環遍歷所有的worklist
initialise(worklist)
worklist <- empty // 初始化一個空的列表
mark():
while not isEmpty(worklist) // 如果列表不空
ref <- remove(worklist) // 獲取列表中的引用
for each fld in Pointers(ref) // 遍歷該引用下所有的子引用
child <- *fld
if child != null && not isMarked(child) // 如果沒有標記則添加標記並添加到worklist中
setMarked(child)
add(worklist, child)
對於該單線程的回收器而言,主要是以深度優先遍歷的方式,將標記這些能夠遍歷到的對象。標記過程相對比較直觀,即先遍歷整個roots,然後再工作列表中獲取對應的子節點的對象的引用,然後對其所引用的其它對象進行標記,直到該工作列表爲空位置,此時標記完成的結束條件就是工作列表爲空,此時回收器就完成了將每個可達的對象的訪問並標記,其餘沒有打上標記的對象就都是需要回收的對象。
清掃
在標記完成之後,在清掃階段就主要是回收器將所有沒有被標記的對象返還給分配器,在這一過程中,回收器會在進行一個線性掃描,即開始釋放未標記的對象,同時清空存活對象的標記位以便下次回收過程複用。
sweep(start, end): // 開始清掃的起止位置
scan <- start
while scan < end // 如果還沒有結束
if isMarked(scan) // 檢查是否標記 如果標記
unsetMarked(scan) // 則設置爲未標記
else free(scan) // 如果未標記則釋放掉該對象
scan <- nextObject(scan) // 獲取下一個對象
在這個實現的過程中,內存的佈局過程需要滿足一定的條件才能完成如上的執行流程,首先,回收不會移動的對象,內存管理必須能夠控制堆的碎片的問題,否則會因爲過多的內存碎片可能會導致分配器無法滿足新分配請求從而增加垃圾回收的頻率,再者,回收器需要能夠遍歷到每一個所有分配出去的對象,即對於給定的對象必須能夠獲取到下一個對象,因此需要獲取較多的數據內存。
標記清除算法示例
package main
import (
"fmt"
"sync"
)
type Obj struct {
value interface{}
size int
prev *Obj
mark bool
childrens []*Obj
}
type GC struct {
mutx sync.Mutex
root *Obj
}
func(gc *GC) register_pool(obj *Obj){
gc.mutx.Lock()
gc.root.childrens = append(gc.root.childrens, obj)
gc.mutx.Unlock()
}
func(gc *GC) search_node_mark(node *Obj){
if node == nil{
return
}
for _, n := range node.childrens {
if n.value == nil {
n.mark = true
}
gc.search_node_mark(n)
}
}
func(gc *GC) search_available_obj(node *Obj)*Obj{
if node == nil {
return nil
}
for _, n := range node.childrens {
if n.value == nil {
n.size = 0
return n
}
r := gc.search_available_obj(n)
if r != nil {
r.size = 0
return r
}
}
return nil
}
func(gc *GC) mark(){
gc.search_node_mark(gc.root)
}
type Worker struct {
node *Obj
gc *GC
}
func(w *Worker) alloc_mem(size int, val interface{}) *Obj{
var obj *Obj
obj = w.gc.search_available_obj(w.node)
if obj != nil {
obj.size = size
obj.value = val
return obj
}
obj = &Obj{
size: size,
value: val,
mark: false,
childrens: []*Obj{},
}
w.node.childrens = append(w.node.childrens, obj)
return obj
}
func(w *Worker) free_mem(obj *Obj){
obj.value = nil
}
func NewGC()* GC{
return &GC{
root:&Obj{
size: 0,
value: "root",
mark: false,
childrens: []*Obj{},
},
}
}
func NewWorker(gc *GC)* Worker{
return &Worker{
&Obj{
size: 0,
value: "",
mark: false,
childrens: []*Obj{},
},
gc,
}
}
func main() {
gc := NewGC()
w1 := NewWorker(gc)
gc.register_pool(w1.node)
obj1 := w1.alloc_mem(10, "obj1")
obj2 := w1.alloc_mem(20, "obj2")
fmt.Println(obj1, obj2)
w1.free_mem(obj1)
gc.mark()
for _, n := range w1.node.childrens {
fmt.Println("obj ", n)
}
obj3 := w1.alloc_mem(30, "new val OBj3")
fmt.Println("obj3 ", obj3)
gc.mark()
for _, n := range w1.node.childrens {
fmt.Println(n)
}
}
該段代碼,只是在簡單的模擬了一下,gc執行過程中的申請內存,釋放內存,標記的一個簡單思路,即通過一個列表加數組的結構把所有的使用的對象,通過一個層級結構來進行連接,但是該結構中需要自己保證在申請內存的時候的對象不能形成類似與聯通圖的情況,否則掃描標記的情況就會一直循環下去,在查找的過程中使用深度優先的策略,去標記未使用的obj和查找當前可用的obj。如果該內容由多個協程來同時進行申請或者釋放obj,最後垃圾回收的代碼就需要進行STW來進行操作,即所有mark的塊的清理的工作都必須在其他協程不工作的前提下來完成,這個示例代碼看後續能否再改進一下,因爲示例代碼壓根就沒有collect的過程。
總結
本文主要是翻閱內存管理相關的書籍,記下相關的內容來加深印象,標記清掃回收的最基本的思路,就是每次申請內存的時候,檢查一下當前是否還有可以使用的內存(前提是提前申請一大塊內存)如果沒有則進行垃圾回收,此時的垃圾回收會依次遍歷已經分配的內存塊是否存活,如果不存活則標記等待下一步的回收操作,最終來獲取一塊可用的內存返回,這裏面還有還多如申請內存的內存對齊,內存碎片的管理等等工作都是比較深入的技術細節點,大家可以自行查閱相關知識。由於本人才疏學淺,如有錯誤請批評指正。