什么是 GC
GC 指一种自动的存储器管理机制,当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。
-from wiki
垃圾回收算法有哪些 分别的优缺点
================
引用计数法
对每个对象设置引用计数,当对象被引用 +1,失去引用/销毁 -1,当计数为 0 的时回收对象内存
优点:简单直接,回收速度快
缺点:需要额外空间维护引用计数,无法解决对象的循环引用问题
标记清除法
从根对象开始遍历所有引用对象,引用的对象打上标记 tag,遍历完成,将没有标记的进行回收
优点:解决引用计数法的缺点
缺点:会产生大量不连续的内存碎片,导致无法给大对象分配内存
标记整理法
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
优点:不会产生内存碎片
缺点:需要移动大量对象,处理效率比较低。
#复制
—
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理
优点:不会产生内存碎片,每次清除针对的都是整块内存
缺点:只使用了内存的一半、移动对象需要耗费时间,效率低于标记清除法
分代收集法
按照对象的生命周期长短划分代空间,生命周期长的放在老年代,生命周期短的放在新生代
优点:回收性能好
缺点:算法复杂
go 的垃圾回收采用是哪个 GC 方法
================
go 采用的是标记清除法,核心就是标记出哪些是内存还在使用(被引用的),哪些内存不再使用(未被引用),把未被引用的内存回收掉,供后续内存分配使用。
暂时无法在飞书文档外展示此内容
特殊 case:如果内存块存放的是指针,那还需要递归的进行标记,全部标记完后,只保留标记的内存,未被标记的内存全部进行回收
为什么 Go 采用标记清除法,而不是其他的方法?
======================
引用计数无法解决循环引用,排除
标记整理好处在于解决内存碎片化的问题,但是 Go 运行时的分配算法基于 tcmalloc,基本上没有碎片问题,对于 gc 并没有提升
复制只能用一半的内存,还需要大量移动,效率低
分代收集的话也不适用,因为 go 的 gc 主要目标是新创建的对象上,即存活时间短更利回收,而不是频繁的检查所有对象
逃逸分析:编译器决定内存分配的位置,不需要程序员指定。函数中申请一个新的对象
如果分配到栈,则函数执行结束就可自动将内存回收
如果分配到堆,则函数执行结束可交给 GC(垃圾回收) 处理
go 编译器的逃逸分析,将大部分新生对象存储在栈里面,直接被回收,生命周期短的对象直接回收并不需要 gc 处理,长期存在的/比较大的对象会分配到堆中,才被 gc 回收,所以分代回收并没有实质上提升
什么是三色标记法?mark-sweep
===================
人为的用三种颜色好描述 go 的 gc 过程,内存中的对象并无颜色区分
三色对应了垃圾回收中的三种状态:
灰色:对象放入“标记队列”中等待(待处理的对象)
黑色:对象已被标记为使用
白色:对象未被标记
步骤:
-
开始 gc 初,所有对象放入白色队列
-
从根对象开始遍历,将所有可达的对象,标记为灰色,放入灰色队列(待处理队列)
-
从灰色队列中取出灰色对象,将它引用的对象标记灰色放入灰色队列,它自己标黑色,放入黑色队列
-
重复步骤 3,直到灰色队列为空,这时候白色对象是不可达对象,回收白色对象
什么是根对象,根对象有哪些?
==============
-
全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量
-
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针
-
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块
怎么的条件会触发 Go 的 GC
=============
- GOGC threshold
每次内存分配时检查当前内存分配量是否达到阀值,达到则会触发 gc
阀值 = 上次 gc 内存分配量 * 内存增长率
内存增长率是由环境变量 GCGO 控制,默认是 100,即当内存扩大一倍的时候启动 gc
- runtime.GC()
类似 Java 的 system.gc api 手动代码触发 gc
- runtime.forcegcperiod (2min)
强制定期 gc,默认 2min 触发一次 gc,在 runtime/proc.go:forcegcperiod
go 的 GC 有哪些优化
==========
标记 - 清理需要 stw,需要暂停所有的 goruntine,做 gc 然后再恢复。
减少 stw 时间,可以提升 go 的 gc 性能
写屏障 (Write Barrier)
本质就是每次内存写操作时候,额外执行一小段代码
写屏障就是让 goroutine 与 GC 同时运行的手段,虽然写屏障不能完全消除 stw,但是可以大大减少 stw 时间,类似开关,gc 的特定时候开启,开启后指针传递时,把指针标记,即本轮不回收,下次 gc 再确定
辅助 GC(Mutator Assist)
为了防止内存分配过快,在 GC 执行过程中,如果 goroutine 需要分配内存,那么这个 goroutine 会参与一部分 GC 的 工作,即帮助 GC 做一部分工作,这个机制叫作 Mutator Assist
代码 GC 编程
多制造 inline 的机会,将新对象尽可能都分配到栈而不是堆,因为 go 实现了退栈即释放,不影响 gc
代码减少逃逸分析:
-
尽量使用局部变量(编译器会根据变量是否被外部引用来决定是否逃逸)
-
参数、返回数值传递值(传指针还是数值,需要修改原值或者内存比较大结构体传指针,而对于只读的占内存较少的结构体,传值获取较好性能)
代码简单直白,制造 inline 机会
减少分配次数
a = make([]int, 0, 1234)
b = make(map[string]int, 2048)
缓存对象
什么是 inline
from wiki
通过参数-gflags=“-m”查看
func add (x, y int) {
return x + y
}
func main() {
x := 1
y := 2
a := add(x,y)
fmt.println(a)
}
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
func main() {
x := 1
y := 2
//inline function add replace by the body of the function
a := x + y
fmt.println(a)
}
不用使用内联的 case:闭包调用、select、for、defer、go 关键词创建的协程
总结:采用越简单的实现,对于傻瓜式语言性能越好
逃逸分析
通过命令 go build -gcflags ‘-m’命令查看
var refs = make([]*int, 32)
func fc() {
refs[0] = new(int)
}
func main() {
fc()
}