垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收掉,以供后续内存分配时使用。
垃圾回收
常见的垃圾回收算法:
- 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1;当引用计数器为0时回收该对象。
- 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
- 缺点:不能很好的处理循环引用,而且实时维护引用计数,也有一定的代价。
- 代表语言:Python、PHP、Swift
- 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为”被引用”,没有被标记的进行回收。
- 优点:解决了引用计数的缺点。
- 缺点:需要STW,即要暂时停掉程序运行。
- 代表语言:Golang(其采用三色标记法)
- 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收频率。
- 优点:回收性能好
- 缺点:算法复杂
- 代表语言:JAVA
go垃圾回收
垃圾回收开始时从root对象开始扫描,把root对象引用的内存标记为“被引用”(需递归处理指针);全部标记完后,未标记的全部标识为未分配。
在span结构中有分配内存的位标记:
gcmarkBits记录每块内存标记情况,其结构与allocaBits完全一样;标记结束时,将allocBits指向gcmarkBits,gcmarkBits在下次标记时重新分配。
三色标记法(回收过程中三种状态):
- 灰色:对象还在标记队列中等待;
- 黑色:对象已被标记(gcmarkBits对应位为1)
- 白色:对象未被标记(gcmarkBits对应位为0),会被GC回收掉。
回收优化
STW(StopTheWorld)就是停掉所有的goroutine,专心做垃圾回收,待垃圾回收结束后再恢复goroutine。
写屏障(Write Barrier):是让goroutine与GC同时运行的手段;在GC特定时机开启,开启后指针传递时会把指针标记(本轮不回收);
辅助GC(Mutator Assist):GC执行过程中,若goroutine需要分配内存,则让此goroutine参与一部分GC的工作。
触发时机
当内存分配量达到阈值:
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由GOGC控制,默认为100,即内存扩大一倍时启动GC。
定期触发:默认情况下,最长2分钟触发一次GC。
runtime.GC(),可手动触发GC。
逃逸分析
逃逸分析(Escape analysis)是指由编译器决定内存分配位置;编译器根据对象是否被函数外部引用决定是否逃逸:
- 若函数外部没有引用,优先放在栈中(大对象,超过栈存储能力时,会被放到堆中);
- 若函数外部存在引用,必定放在堆中;
函数参数为interface类型,编译期间很难确定具体类型,也会产生逃逸;闭包中引用的局部变量也会产生逃逸;
- 栈上分配内存比在堆中分配内存有更高的效率
- 栈上分配的内存不需要GC处理
- 堆上分配的内存使用完毕会交给GC处理
- 逃逸分析目的是决定内分配地址是栈还是堆
- 逃逸分析在编译阶段完成
传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。
代码优化
为提高性能,需要减少对象的分配(增加重用,降低GC压力):
- 减少对象分配:
- 尽量重用对象;
- 当有很多小对象时,可考虑封装成一个大的对象(通过struct组合对象,GC在扫描时,只需判断整个对象是否有效即可);
- slice与map等预分配内存;
- string与byte[]间转换时,会涉及到内存的重新分配与数据复制,尽量减少;
- 避免大量字符串连接操作(通过+连接):
- fmt.Sprintf:方便拼接字符串;
- strings.Join:方便串联字符串数组;
- strings.Builder:方便字符串连接,尽量预分配容量;
- 切片尽量预分配容量:减少append过程中的自动增长引起的内存重分配与数据复制;
- 及时清理(通过defer关闭句柄等);