1、简述 Go 语言GC(垃圾回收)的工作原理
Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。
1. 标记清除法
分为两个阶段:标记和清除
- 标记阶段:从根对象出发寻找并标记所有存活的对象;
- 清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表;
- 缺点是需要暂停程序STW。
2. 三色标记法:
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了写屏障技术,当对象新增或者更新会将其着色为灰色。
一次完整的GC分为四个阶段:
- 准备标记(需要STW),开启写屏障。
- 开始标记
- 标记结束(STW),关闭写屏障
- 清理(并发)
基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。
混合写屏障分为以下四步:
- GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);
- GC期间,任何栈上创建的新对象均为黑色;
- 被删除引用的对象标记为灰色;
- 被添加引用的对象标记为灰色;
- 总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。
2、Go 中 make 和 new 的区别?
- 共同点:给变量分配内存。
- 不同点:
1.作用变量类型不同,new给string,int和数组分配内存,make给切片,map,channel分配内存;
2.返回类型不一样,new返回指向变量的指针,make返回变量本身;
3.new 分配的空间被清零。make 分配空间后,会进行初始化。
3、数组和切片的区别?
- 相同点:
1.只能存储一组相同类型的数据结构;
2.都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取。 - 不同点:
1.数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容;
2.数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变。
4、能介绍下 rune 类型吗?
golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8。
- byte 等同于int8,常用来处理ascii字符;
- rune 等同于int32,常用来处理unicode或utf-8字符。
5、Go 的 slice 底层数据结构和一些特性?
Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。
slice 的主要实现是扩容。对于 append 向 slice 添加元素时,假如 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。
对于切片的扩容规则:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费,网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。
6、Go 的 defer 底层数据结构和特性?
每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单链表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。
defer 的规则总结:
- 延迟函数的参数是 defer 语句出现的时候就已经确定;
- 延迟函数执行按照后进先出的顺序执行;
- 延迟函数可能操作主函数的返回值;
- 申请资源后立即使用 defer 关闭资源。