七叶笔记 » golang编程 » golang内存回收

golang内存回收

所谓内存回收,便是指当前内存使用完毕,释放当前存储器,以供后续继续使用,如果没有进行及时的释放,则会造成 内存泄漏

编写 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(该对象不会被清理)

标记过程

  1. GC 开始时,认为所有的对象都是 白色 ,即垃圾。
  2. 从根开始遍历,被触达的对象置成 灰色
  3. 遍历所有灰色对象,将他们内部的引用变量置成 灰色 ,自身置成 黑色
  4. 循环第 3 步,直到没有灰色对象了,只剩下了黑白两种,白色的都是垃圾。
  5. 对于黑色对象,如果在标记期间发生了写操作,写屏障会在真正赋值前将新对象标记为灰色
  6. 标记过程中,内存 新分配的对象,会先被标记成黑色再返回。

假设ab分别引用了AB, B引用了D

初始状态全部标记为白色

从ab开始扫描后将对象AB置灰

分析AB后将AB置黑,将D置灰

再次分析D,发现D没有引用其他对象,将D置黑

保留黑色对象,白色对象会被回收

为了能让标记过程也能并行,Go 采用了三色标记 + 写屏障的机制。标记清除算法致命的缺点就在 STW 上,所以 Golang 后期的很多优化都是针对 STW 的,尽可能缩短它的时间,避免出现 Go 服务的卡顿。它的步骤大致如下:

  1. GC 开始,开启写屏障等准备工作。开启写屏障等准备工作需要短暂的 STW。
  2. Stack scan 阶段,从全局空间和 goroutine 栈空间上收集变量。
  3. Mark 阶段,执行上述的三色标记法,直到没有灰色对象为止。
  4. Mark termination 阶段,开启 STW,回头重新扫描 root 区域新变量,对他们进行标记。
  5. 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 函数来进行。

相关文章