从根对象的收集来看,会将全局变量、析构器、所有协程的栈进行扫描。从而标记目前还在使用的内存对象。 下一步是要从这些标记为灰色的内存对象出发, 进一步标记整个堆内存中活着的对象。
在之前进行根对象扫描的时候,会将标记的对象放入到本地队列中,而如果本地队列放不下,就放入全局队列中。这种设计最大限度的防止了使用锁,而在本地缓存的队列可以被逻辑处理器P无锁访问。
在进行扫描时,使用相同的原理,首先消费本地队列中找到的标记对象,如果本地队列为空后,则加锁获取全局队列。
在标记期间、会循环往复的从本地标记队列获取到灰色对象,灰色对象扫描到的白色对象仍然会放入标记队列中,而如果扫描到已经被标记的对象,则进行忽略,一直到队列中任务为空为止。
for !(preemptible && gp.preempt) {
// 从本地标记队列中获取对象, 获取不到则从全局标记队列获取
b := gcw.tryGetFast()
if b == 0 {
// 阻塞获取
b = gcw.tryGet()
}
// 扫描获取到的对象
scanobject(b, gcw)
...
}
对象的扫描过程位于scanobject函数中。之前介绍过,堆上的任意一个指针能够找到其对象所在span中的位置,并且对象有没有被扫描,我们也可以通过gcmarkBits标志得出。但现在面对的问题是需要对整个对象进行扫描,查看对象中是否含有指针。这仍然依靠的是在分配时就记录了对象中是否包含指针等信息。之前介绍过,heapArena 包含了 整个64M大小 的Arena元数据。
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
其中包含一个重要的字段,用位图的形式记录了每一个指针大小(8byte)的内存信息。每一个指针大小的内存都会有两个bit分别表示是否应该继续扫描和是否包含指针。
bitmap [heapArenaBitmapBytes]byte
如下所示, bitmap代表一个位图,其一个byte大小的空间中,对应了虚拟内存4个指针大小的空间。
bitmap中前4位为扫描位,后4位为指针位。分别对应了指定的指针大小空间是否需要继续进行扫描以及是否包含指针。
例如,对于一个结构体obj, 当我们知道其前2个字段为指针,后面的字段以及不包含指针以后,那么后面的字段就不再需要扫描。因此,扫描位可以加速对象的扫描,避免扫描无用的字段。
type obj struct{
*int a
*T b
intc
float d
}
当发现需要继续扫描并且发现了当前有指针时,就需要取出指针的值,并对其进行扫描,如果发现引用的是堆中的白色对象(即还没有被标记),则标记将该对象(表明此时已经为灰色) 并将该对象放入本地任务队列中。
当完成并发标记阶段所有灰色对象的扫描和标记,则进入到标记终止阶段。标记终止阶段会再次进入STW,标记终止阶段主要完成一些指标例如用时的统计、统计强制开始GC的次数、更新下一次触发gc需要达到的目标、关闭写屏障、并唤醒后台清扫的协程,开始下一阶段的清扫工作。
在标记终止阶段重要的任务是计算下一次触发垃圾回收时需要达到的堆目标,这叫做垃圾回收的调步算法,下一小节中将会详细介绍。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8