所谓内存回收,便是指当前内存使用完毕,释放当前存储器,以供后续继续使用,如果没有进行及时的释放,则会造成 内存泄漏
编写 go 代码不需要像写 C/ c++ 那样手动的 malloc 和 free 内存,因为 malloc 操作由 Go 编译器的逃逸分析机制帮我们加上了,而 free 动作则是有 GC 机制来完成。
常见的GC方式有如下三种
引用计数:对每一个对象维护一个引用计数,当引用该对象的对象被销毁的时候,引用计数减1,当引用计数为0的时候,回收该对象,比如c++的shared_ptr
优点 :对象可以很快地被回收,不会出现内存耗尽或达到某个阀值时才回收。
缺点:不能很好的处理循环引用,而且实时维护引用计数,也有一定的代价。
代表语言:Python,PHP, Swift
标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为”被引用”,没有被标记的进行回收。
优点:解决引用计数的缺点
缺点:需要STW(stop the word),即要暂停程序运行
代表语言:Golang,目前最新1.8版本采用的是三色标注法
分代收集:按照生命周期进行划分不同的代空间,生命周期长的放入老年代,短的放入新生代,新生代的回收频率高于老年代的频率
代表语言: JAVA
三色标记法
三色标记法基于可达性算法
可达性分析算法:目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC root s”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
三色标记法颜色定义
1、白色:初始状态下所有的对象都是白色,gcmarkBits对应的位为0(该对象会被清理)
2、灰色:对象被进行标记,但是这个对象的子对象(也就是它引用的对象)未被进行标记
3、黑色:对象被标记同时这个对象引用的子对象也被进行标记gcmarkBits对应的位为1(该对象不会被清理)
标记过程
- GC 开始时,认为所有的对象都是 白色 ,即垃圾。
- 从根开始遍历,被触达的对象置成 灰色 。
- 遍历所有灰色对象,将他们内部的引用变量置成 灰色 ,自身置成 黑色
- 循环第 3 步,直到没有灰色对象了,只剩下了黑白两种,白色的都是垃圾。
- 对于黑色对象,如果在标记期间发生了写操作,写屏障会在真正赋值前将新对象标记为灰色 。
- 标记过程中,内存 新分配的对象,会先被标记成黑色再返回。
假设ab分别引用了AB, B引用了D
初始状态全部标记为白色
从ab开始扫描后将对象AB置灰
分析AB后将AB置黑,将D置灰
再次分析D,发现D没有引用其他对象,将D置黑
保留黑色对象,白色对象会被回收
为了能让标记过程也能并行,Go 采用了三色标记 + 写屏障的机制。标记清除算法致命的缺点就在 STW 上,所以 Golang 后期的很多优化都是针对 STW 的,尽可能缩短它的时间,避免出现 Go 服务的卡顿。它的步骤大致如下:
- GC 开始,开启写屏障等准备工作。开启写屏障等准备工作需要短暂的 STW。
- Stack scan 阶段,从全局空间和 goroutine 栈空间上收集变量。
- Mark 阶段,执行上述的三色标记法,直到没有灰色对象为止。
- Mark termination 阶段,开启 STW,回头重新扫描 root 区域新变量,对他们进行标记。
- Sweep 阶段,关闭 STW 和 写屏障,对白色对象进行清除。
标记过程中,如果堆上的对象被赋值给了一个 栈上指针 ,会导致这个对象没有被标记到。 因为对栈上指针进行写入,写屏障是检测不到(主动忽略)的 。为了解决这个问题,标记的最后阶段,还会回头重新扫描一下所有的栈空间,确保没有遗漏, 如果系统的 goroutine 很多,这个阶段耗时也会比较长,甚至会长达 100ms。
Go 在 1.8 版本引入了 混合写屏障 ,其会在赋值前,对旧数据置灰,再视情况对新值进行置灰。这样就不需要在最后回头重新扫描所有 Goroutine 的栈空间了,这使得整个 GC 过程 STW 几乎可以忽略不计了。混合写屏障会有一点小小的代价,就是在扫描过程中有的对象 的确变成了垃圾,而我们却把他置灰了,使得对象只能等到下一轮 GC 才能被回收了。 GC 过程创建的新对象直接标记成黑色也会带来这个问题,即使新对象在扫描结束前变成了垃圾,这次 GC 也不会回收它,只能等下一轮。
gc性能优化技巧
硬性参数
涉及算法的问题,总是会有些参数。GOGC 参数主要控制的是 下一次 gc 开始的时候的内存使用量 。
比如当前的程序使用了 4M 的内存(这里说的是 堆内存 ),即是说程序当前 reachable 的内存为 4m,当程序占用的内存达到 reachable*(1+GOGC/100)=8M 的时候,gc 就会被触发,开始进行相关的 gc 操作。
如何对 GOGC 的参数进行设置,要根据生产情况中的实际场景来定,比如 GOGC 参数提升,来减少 GC 的频率。
减少对象分配 所谓减少对象的分配,实际上是尽量做到,对象的重用。 比如像如下的两个函数:
第一个函数没有 形参 ,每次调用的时候返回一个 [] byte ,第二个函数在每次调用的时候,形参是一个 buf []byte 类型的对象,之后返回读入的 byte 的数目。
第一个函数在每次调用的时候都会分配一段空间,这会给 gc 造成额外的压力。第二个函数在每次调用的时候,会重用形参声明。
string 与 []byte 转化 在 stirng 与 []byte 之间进行转换,会给 gc 造成压力 通过 gdb,可以先对比下两者的数据结构:
两者发生转换的时候,底层数据结结构会进行复制,因此导致 gc 效率会变低。解决策略上,一种方式是一直使用 []byte,特别是在数据传输方面,[]byte 中也包含着许多 string 会常用到的有效的操作。另一种是使用更为底层的操作直接进行转化,避免复制行为的发生。
关于 uintptr 的底层类型是 int,它可以装下指针所指的地址的值。它可以和 unsafe.Pointer 进行相互转化,主要的区别是,uintptr 可以参与指针运算,而 unsafe.Pointer 只能进行指针转化,不能进行指针运算。想要用 golang 进行指针运算,可以参考这个。具体指针运算的时候,要先转成 uintptr 的类型,才能进一步计算,比如偏移多少之类的。
少量使用+连接 string 由于采用 + 来进行 string 的连接会生成新的对象,降低 gc 的效率,好的方式是通过 append 函数来进行。