Go垃圾回收[4]-根对象与全局扫描

467次阅读  |  发布于2年以前

choose the tool for the problem and do not try to fit the problem for the tool

前情回顾

根对象

在上文中介绍了并行标记的执行模型,后面的几个小节将具体介绍标记“染色”阶段发生的故事。

扫描的第一阶段是要扫描根对象。在最开始的标记准备阶段,就会统计这次GC一共要扫描多少的对象。每一个具体的序号对应着要扫描的对象和类型。

job := atomic.Xadd(&work.markrootNext, +1) - 1

work.markrootNext 会原子的增加,这是因为可能会有多个后台标记协程同时访问该变量。而这也保证了多个后台标记协程 会执行不同的任务。

那么何为根对象呢?对于三色标记来说,根对象是最基本的对象,从根对象出发,可以找到所有的引用对象即活着的对象。在Go语言中,根对象包括了全局变量(在.bss 和.data段内存中)、span中的finalizer的任务数量、以及所有的协程栈。finalizer是Go语言中和某种对象绑定的析构器。当某些对象的内存释放后,需要调用析构器函数,从而完整释放资源。例如os.File对象使用了析构器函数关闭操作系统文件描述符,即便用户忘记了调用close()方法也会释放操作系统资源。

全局变量扫描

扫描全局变量需要编译时与运行时的共同努力。在运行时,才能确定全局变量分配到了虚拟内存哪一个区域,并且如果全局变量有指针的话,在运行时其指针指向的内存可能会变化。而在编译时,可以确定全局变量中哪些位置包含指针,信息位于位图ptrmask字段中。ptrmask的每一个bit位都对应了.data段中的一个指针大小(8byte),当bit位为1代表当前位置是一个指针。知道了指针可能的位置,这时,需要 求出 当前的指针在堆区的哪一个对象上,并将当前对象标记为灰色。

有些读者可能会觉得不可思议,如何能够通过指针找到指针所在的对象位置呢。这靠的是Go语言内存分配时,对内存的精细化管理。对一个指针,首先找到其在哪一个heapArena当中。heapArena是每一次向操作系统申请分配的最小64M大小的区域。

type mheap struct {
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}

heapArena结构存储了许多元数据,其中包括了每一个page ID(8 KB) 对应的mspan

spans [pagesPerArena]*mspan

所以,可以通过指针的位置找到其对应的mspan,并进而找到其位于mspan中第几个元素中。当找到此元素确实存在后,会选择将gcmarkBits对应元素的bit设置为1。表明其已经被标记。同时,将该元素(对象)放入标记队列中。

在span中,每一个元素在位图gcmarkBits中都会有标志位表明当前的元素中的对象是否被标记。

finalizer

之前提到finalizer是特殊的对象,其是在对象释放后会调用的析构器,用于资源释放。因为析构器不会被栈上或全局变量引用,需要单独处理。

在标记期间,会遍历mspan中的specials链表,扫描finalizer所位于的元素(对象),

for sp := s.specials; sp != nil; sp = sp.next {
            if sp.kind != _KindSpecialFinalizer {
                continue
            }
            ...
            scanobject(p, gcw)
            scanblock(uintptr(unsafe.Pointer(&spf.fn)), sys.PtrSize, &oneptrmask[0], gcw, nil)
}

并对当前元素(对象)进行扫描,扫描对象的详细过程将在下一小节介绍。注意在这里,并不能把finalizer所位于的Span中的对象加入到根对象中,否则我们将失去回收该对象的机会。同时需要扫描析构器结构中的字段fn,因为fn可能指向了堆中的内存,并可能会被回收。

各位看官,我们下周再见,see you~

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8