简单了解
GC 与 mutator 线程并发运行,允许多个 GC 线程并行运行
在 GC 的过程中同时运行的 G 称为
mutator
,mutator assist
机制就是 G 辅助 GC 做一部分工作的机制。
辅助 GC 做的工作有两种类型,一种是标记(
Mark
),另一种是清除(Sweep
)。
GC 是一个使用写屏障的并发标记和清除。
GC 是非分代的,非紧凑的。
Allocation 是按照大小隔离每个P
分配的区域来完成的,以在消除常见情况下的锁的同时,最小化碎片。
GC算法的高级描述
了解 GC 的好地方,可以从 Richard Jones 的gchandbook.org
开始。
1. GC 执行清除终止
a. Stop the world
,这将导致所有 P 达到 GC 安全点。
b. 清除任何未清除过的spans
,只有在预期时间之前强制执行此GC
周期时,才会有未清除的span
。
2. GC 执行标记阶段
a. 准备标记阶段,将gcphase
设置为_GCmark
(从_GCoff
开始),启用写屏障,启用mutator assist
,并对根标记作业进行排队。
在所有P
都启用写屏障之前,不会扫描任何对象,这是使用STW
完成的。
b. Start the world
,从现在开始,GC 工作由调度器启动的标记worker
和作为allocation
的一部分执行的assists
来完成。
写屏障将覆写的指针和任何指针写的新指针值都着色。
新分配的对象立即被标记为黑色。
c. GC 执行根标记作业。包括:扫描所有栈
,着色所有全局变量
,以及着色堆外运行时数据结构中的任何堆指针
。
扫描栈会停止goroutine,对goroutine栈中找到的任何指针进行着色,然后恢复goroutine。
d. GC 耗尽灰色对象的工作队列,将每个灰色
对象扫描为黑色
,并对在该对象中找到的所有指针进行着色(反过来可能会将这些指针添加到工作队列中)。
e. 由于 GC work 分散在本地缓存中,因此 GC 使用分布式终止算法
来检测何时不再有根标记作业或灰色对象(参见gcMarkDone
函数)。
此时,GC 状态转换到标记终止(gcMarkTermination
)。
3. GC 执行标记终止gcMarkTermination
a. Stop the world
b. 将gcphase
设置为_GCmarktermination
,并禁用 workers 和 assists。
func gcMarkTermination(nextTriggerRatio float64) {
// World is stopped.
// Start marktermination which includes enabling the write barrier.
atomic.Store(&gcBlackenEnabled, 0)
setGCPhase(_GCmarktermination)
......
}
//go:nosplit
func setGCPhase(x uint32) {
atomic.Store(&gcphase, x)
writeBarrier.needed = gcphase == _GCmark || gcphase == _GCmarktermination
//启动写屏障
writeBarrier.enabled = writeBarrier.needed || writeBarrier.cgo
}
c. 进行内务整理,如flushing mcaches
确保所有的
mcaches
都被flushed
,每个P
将在分配之前刷新自己的mcache
,但是空闲的P
可能不会。
因为这对于清除所有的span
是必要的,所以我们需要确保在下一个 GC 周期开始之前flush完
所有mcaches
。
mcache:
是小对象的每个线程缓存(在Go
中是per-P
)。
不需要锁,因为它是每个线程(per-P
)。
mcaches
是从非 gc 内存
中分配的,因此必须对任何堆指针进行特殊处理。
type p struct {
lock mutex
id int32
status uint32 // one of pidle/prunning/...
link puintptr
schedtick uint32 // incremented on every scheduler call
syscalltick uint32 // incremented on every system call
sysmontick sysmontick // last tick observed by sysmon
m muintptr // back-link to associated m (nil if idle)
mcache *mcache //一个结构体
}
4. GC 执行清除阶段
a. 准备清除阶段,将gcphase
设置为_GCoff
,设置清除状态并禁用写屏障。
b. Start the world
,从现在开始,新分配的对象是白色的,如有必要,在使用spans
前allocating
清除spans
。
c. GC 在后台进行并发清除
并响应allocation
,见下面的描述。
5. 当分配足够时,重复上面 1 开始的步骤,参见下面关于GC rate
的讨论。
并发清除
清除阶段与正常程序执行并发进行。
在后台goroutine
中,堆被惰性(当 goroutine 需要另一个span
时)且并发地逐个span
扫描(这有助于不是CPU bound
的程序)。
在STW 标记终止
的结尾,所有的span
都被标记为需要清除
。
后台清除器 goroutine 简单地逐个清除span
。
为了避免在存在未清除的span
时请求更多的OS内存
,当goroutine
需要另一个span
时,它首先尝试通过清除来回收这些内存。
当 goroutine 需要分配一个新的小对象span
时,它会清除相同大小的小对象span
,直到释放至少一个对象为止。
当 goroutine 需要从堆中分配大对象span
时,它会清除span
,直到将至少那么多页面释放到堆中。
有一种情况,这可能是不够的:如果 goroutine 清除并释放两个不相邻的单页span
到堆中,那么它将分配一个新的双页span
,但是仍然可以有其他单页未清除的span
,可以组合成双页的span
。
确保在未清除的span
上不进行任何操作(这会破坏 GC 位图中的标记位)至关重要。
在 GC 期间,所有mcache
都被刷新到中央缓存
中,因此它们是空的。
当一个 goroutine 抓取一个新的span
到mcache
时,goroutine
会清除mcache
。
当 goroutine 显式释放对象或设置finalizer
时,goroutine 确保span
已经清除(通过清除或者等待并发清除完成)。
finalizer goroutine
仅在所有span
已经清除时才开始。
当下一次 GC 启动时,它将清除所有尚未清除的span
(如果有的话)。
GC rate
下一次 GC 是在我们分配了与已经使用的内存成正比的额外内存量之后。
该比例由GOGC
环境变量控制(默认为100
)。
如果GOGC=100
,而我们使用的是4M
,那么当达到8M
时,我们将再次进行 GC(此标记在next_gc
变量中被跟踪)。
next_gc 在
startCycle
函数中被重新计算,即每次gc周期
都通过GOGC
重新计算下一次gc内存阈值。
获取GOGC
:
func readgogc() int32 {
p := gogetenv("GOGC")
if p == "off" {
return -1
}
if n, ok := atoi32(p); ok {
return n
}
return 100
}
这使得GC成本
与allocation 成本
成线性比例。
调整GOGC
只会改变线性常量(以及使用的额外内存量)。
Oblets
为了防止在扫描大型对象时出现长时间的暂停,并提高并行性,垃圾收集器将大于maxObletBytes
的对象的扫描作业分解为最多maxObletBytes
的oblets
。
当扫描遇到大对象时,它只扫描第一个oblet
,并将其余oblets
作为新的扫描作业排队。
术语
oblets
来源:
oblets
是Tcl
语言的一个非常简单的对象系统,对象数据存储在全局数组中。由于
Tcl
对象系统的普及,oblet
主要用于教育目的,供那些希望了解简单的基于数组的对象的基本知识的人使用。
TCL
经常被用于 快速原型开发,脚本编程,GUI 和 测试 等方面。TCL
念作踢叩tickle
。
使用最广泛的TCL扩展
是TK
,TK提供了各种 OS 平台下的图形用户界面GUI
。连强大的Python
语言都不单独提供自己的 GUI,而是提供接口适配到TK
上。
网友评论