gc也就是垃圾回收。最近写的项目,pprof查看性能,发现在gc的消耗非常大,发现gc的cpu占用已经到了30%。在oom的时候更是要2分钟才开始进行gc。
因此想着深入调研一下go的gc模式,了解在写程序时保持哪些好的代码习惯,才能最大的减少程序的gc调用。
目录
gc是什么
了解的同学可以直接跳过。
GC Garbage Collection。直译过来就是垃圾回收。想要进行程序调优,是肯定避不开这个环节的,gc管理不好,很容易造成程序的内存无限增长,然后被系统杀掉,上线的项目发生内存泄漏,肯定是p0级别的问题了。
一般写的程序中会用到两种内存,堆内存和栈内存。堆内存就是堆状数据结构存储的内存,不连续,动态分配,存取慢,系统不会自动帮你释放;栈内存就是连续的内存存储结构,先进后出,存取速度快,仅次于寄存器,但是数据大小和生命周期确定,系统会自动释放的。
像函数中定义的一些局部变量,外部没有办法访问,用完就释放掉,这些栈内存中的数据管理起来相对简单,不需要人工的干预释放,所以GC是不会管栈中的内存的,GC负责清理的只有在堆中的内存。
在C等比较早期的语言中是没有堆内存管理的,在堆中申请和释放内存都需要手动执行,这样很容易出现忘记释放内存的情况,导致内存泄漏。所以诞生了一个更人性化的工具,就是GC,它可以自动管理内存的申请和释放,避免造成内存泄漏。当然,有得必有失,有了GC工具,也就需要额外性能或者内存的开支。
gc的主流算法
gc有两种主流的算法,一个是 【引用计数】,一个是【可达性分析】。只是大类,具体实现的算法就有很多了。
【引用计数】顾名思义,就是对每个使用的变量进行计数统计,每被引用一次,那么计数+1,否则计数-1,到0就可以回收了。
【可达性分析】通过引用的链路来判断是否可以回收,能访问到的就是正在使用的,不能回首;所有不能访问到的,是可以回收的。
当前可达性分析要更主流一些。Go、Java、.Net等都是如此。因为【引用计数】虽然更简单,实时性更好,但是有个很严重的问题,是无法处理循环引用的,比如A->B,B->C,C->A。这种所有的引用均为1,除非主动断开其中一条链,否则不会触发回收。所以【引用计数】的方式一般会和【可达性分析】一起使用。
三色标记法
go的内存回收算法,三色标记法,也是属于可达性分析算法的一种。
先了解一个算法Mark-And-Sweep(标记清扫),这个算法就是严格按照追踪式算法的思路来实现的。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是 0,如果发现对象是可达的就会置为 1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位设置成 0 方便下次清理。
这个算法最大的问题是 GC 执行期间需要把整个程序完全暂停,不能异步进行 GC 操作。因为在不同阶段标记清扫法的标志位 0 和 1 有不同的含义,那么新增的对象无论标记为什么都有可能意外删除这个对象。
对实时性要求高的系统来说,Mark-And-Sweep这种需要长时间挂起的标记清扫法是不可接受的。所以就需要一个算法来解决 GC 运行时程序长时间挂起的问题,三色标记法就是干这个的。
三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个 GC。
注意,三色标记法虽然是异步的,但还是会有中断的时间。
垃圾回收器的工作流程大体如下:
- 标记出哪些对象是存活的,哪些是垃圾(可回收);
- 进行回收(清除/复制/整理),如果有移动过对象(复制/整理),还需要更新引用。
无论使用哪种算法,标记总是必要的一步。而三色标记法的中断时间就在于刚开始标记的时候。所以需要知道一个概念,叫「Stop The World 」,简称「STW」。
三色标记法的算法流程
把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:
- 白色:尚未访问过。
- 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
- 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:
- 初始时,所有对象都在 【白色集合】中;(搜索白色集合需要STW)
- 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
- 从灰色集合中获取对象:
3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
3.2. 将本对象 挪到 【黑色集合】里面。 - 重复步骤3,直至【灰色集合】为