Golang Runtime是Go语言运行所需要的基础设施,主要有以下功能:
- 协程调度,内存分配,GC
- 操作系统及CPU相关的操作的封装 (信号处理、系统调用、寄存器操作、原子操作等),CGO
- pprof,trace,race检测的支持
- map,channel,string等内置类型及反射的实现
垃圾回收 GC
垃圾回收是一种自动内存管理的机制,当程序向操作系统申请的内存使用完毕后,来及回收机制将其回收并供其他代码进行内存申请时复用,或将其归还给操作系统。
GC的作用提现在两个方面:
- 程序员可以更多地关注代码本身,而无需对内存进行手动的申请和释放操作
- GC仅在程序需要进行特殊优化的时候才会现身,是一种自动触发机制
Golang的垃圾回收机制
Go语言的GC目前使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法。Go语言的GC发展历程如下:
版本 | 发布时间 | GC | STW时间 |
---|---|---|---|
v1.1 | 2013/5 | STW | 百ms-几百ms级别 |
v1.3 | 2014/6 | Mark STW,Sweep并行 | 百ms级别 |
v1.5 | 2015/8 | 三色标记法,并发标记清除 | 10ms级别 |
v1.6 | 使用bitmap来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存 | 10ms内 | |
v1.7 | 2ms内 | ||
v1.8 | 2017/2 | hybird write barrier,混合写屏障 | 0.5ms左右 |
v1.9 | 彻底移除了栈的重复扫描过程 | ||
v1.12 | 整合两个阶段的Mart Termination | ||
v1.13 | 着手解决向操作系统归还内存的问题,提出了新的Scavenger | ||
v1.14 | 全新的页分配器替代Scavenger,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的STW时间过长问题。 |
三色标记法
三色标记法是对标记-清扫
算法的改进,主要是针对标记
阶段进行改进:
- 起初所有对象都是白色
- 从根节点出发扫描所有可达对象,标记为灰色,放入待处理队列
- 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
- 重复3,直到灰色队列为空,此时白色对象即为垃圾,进行回收
并发标记清除
三色标记法在扫描之前要执行STW (Stop The World)在扫描完成后要进行STW (Start The World),即在扫描的过程中,所有线程要被暂时冻结,保证被扫描的对象状态不发生改变,STW时间过长的话,会影响GC性能。
Golang的GC和用户代码是并发执行的,这就带来了一个问题,由于用户代码的执行,可能会使得在GC扫描和标记两个阶段之间,某个结点的引用状态发生了改变,很可能将一个实际存活的节点被标记为白色而被误清除,或者某个已经死亡的节点被误标记为存活状态而造成内存泄漏,为了解决这个问题,Go语言引入了写屏障的概念
写屏障、混合写屏障
- 写屏障
写屏障的主要功能就是:在每一轮GC开始时初始化一个“写屏障”,用于保存第一次扫描时各对象的状态,再下一次扫描时,各个对象的状态与上一次扫描的状态进行对比,将引用状态变化的对象标记为灰色,以防其丢失,然后继续处理剩下的对象。写屏障的引入,是为了保证各对象的三色不变性。
- 混合写屏障
为了保证三色状态稳定性,主要是需要避免如下两个情况的出现
- 避免黑色对象引用白色对象 —— 使用Dijkstra插入屏障解决
- 避免灰色对象到达白色对象的路引用路径被破坏 —— 使用Yuasa删除屏障解决
Go语言1.8的时候为了简化GC流程,同时减少标记终止阶段的重扫成本,将Dijkstra插入屏障和Yuasa删除屏障进行混合,形成了混合写屏障。
混合写屏障的基本思想是:对正在被覆盖着色的队形进行着色,且如果当前栈未扫描完成,则同样对指针进行着色。
Go语言中的GC流程
当前版本的Go语言以STW为界限,将GC分为五个阶段:
- 清扫终止阶段 Sweep Termination
为下一个阶段的并发标记做准备,启动写屏障
- 扫描标记阶段 Mark
与赋值器并发执行,写屏障开启
- 标记终止阶段 Mark Termination
保证一个周期内标记任务完成,停止写屏障
- 内存清扫阶段 GCoff
将需要回收的内存归还到堆中,写屏障关闭
- 内存归还阶段 GCoff
有了GC为什么还会出现内存泄漏?
这里的内存泄漏是指:预期的能很快被释放的内存,由于附着在了长期存活的内存上,或生命周期意外地被延长,导致预计能够立即回收的内存长时间得不到回收。
可能出现的情况如下:
内存未按预期得到快速释放
goroutine泄漏
goroutine上会需要消耗一定的内存空间来保存上下文信息以及一些变量信息,当一个goroutine长时间不被结束时,可能造成内存泄漏:
func keepalloc() {
for i:=0;i<10000;i++{
go func(){
select{}
}
}
}