参考书籍
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的过程。
总结
本文主要是翻阅内存管理相关的书籍,记下相关的内容来加深印象,标记清扫回收的最基本的思路,就是每次申请内存的时候,检查一下当前是否还有可以使用的内存(前提是提前申请一大块内存)如果没有则进行垃圾回收,此时的垃圾回收会依次遍历已经分配的内存块是否存活,如果不存活则标记等待下一步的回收操作,最终来获取一块可用的内存返回,这里面还有还多如申请内存的内存对齐,内存碎片的管理等等工作都是比较深入的技术细节点,大家可以自行查阅相关知识。由于本人才疏学浅,如有错误请批评指正。