垃圾回收GC
2021年12月13日 · 2408 字 · 5 分钟 · Java
什么是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()
}