Go自动管理内存

Go垃圾回收机制

垃圾回收(GC,garbage collection)是自动内存管理的一种形式,通常由垃圾收集器收集并适时回收或重用不再被对象占用的内存。所谓垃圾回收,即所有的内存分配都做都会被在运行时记录,同时任何对该内存的使用也都会被记录,然后垃圾回收器会对所有已经分配的内存进行跟踪监测,一旦发现有些内存已经不再被任何人使用,就阶段性地回收这些没人用的内存,当然,因为需要尽量最小化垃圾回收的性能顺好,以及降低对正常程序执行过程的影响,现实中的垃圾回收算法要比这个复杂的多,比如为对象增加年龄属性等(FIFO?),但基本原理都是如此。

C/C++语言的手动管理内存

不支持垃圾回收的语言往往采用手动的方式进行资源管理,然而各种非预期的原因可能会引发严重的问题。

野指针

手动管理内存的其中一个问题就是由于指针的到处传递而无法确定何时可以释放该指针所指向的内存块。如果代码中的某个位置释放了内存,而另一些地方还在使用指向这块内存的指针,那么这些指针就变成了所谓的“野指针”(wild pointer)或者“悬空指针” (dangling pointer),对这些指针进行的任何读写操作都会导致不可预料的后果。

我们来看以下例子:

1
2
3
4
5
6
7
8
9
int main(){
char *p = (char *) malloc(100);
strcpy(p, "hello");
free(p); // p 所指的内存被释放,但是p所指的地址仍然不变https://segmentfault.com/img/bVbCsDx
if(p != NULL) // 没有起到防错作用
{
strcpy(p, "world"); // 出错
}
}

我们可以看到如下情况:

image-20220111034419688

当然,我们在写代码的时候一般不会这么傻,但是如果遇到下面例子,运行代码时就是一种严峻的考验(摘自许式伟《Go语言编程》):

1
2
3
4
5
int* p = new int;
p += 10; // 对指针进行了偏移,因此那块内存不再被引用
// …… 这里可能会发生针对这块int内存的垃圾收集 ……
p -= 10; // 咦,居然又偏移到原来的位置
*p = 10; // 如果有垃圾收集,这里就无法保证可以正常运行了
内存泄漏

当你向系统申请分配内存进行使用,可是使用完后却不归还,结果你申请到的那块内存自己也不会再访问,而系统也不会将其分配给需要的程序,长此以往,造成积重难返的后果。

解藕

当两个模块中同时维护了同一内存时,释放内存将会变得非常小心,这种手动分配的困难在于,难以在本地模块内做出全局的决定。当然C++11之后引入了智能指针,可以使得其更好的完成类似自动垃圾回收的功能。

Go语言的垃圾回收

常见的垃圾回收机制
引用计数法

这个在cmu-15445中也有体现,我们给对象设置一个引用计数,用来表示当前正在访问该对象的或者想要访问该对象的线程的数量,当引用计数为0时,我们就可以回收该对象了。

复制法

复制法将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

标记清除法

原始的标记清除法分为两个步骤:

  1. 标记。先STP(stop the world),暂停整个程序的全部线程,将被引用的对象打上标记。
  2. 清除没有被打标记的对象,即回收内存资源,然后恢复运行线程。
三色标记法

三色标记法中的三色对应垃圾回收过程中对象的三种状态:

  • 灰色:对象还在标记队列中等待
  • 黑色:对象已被标记,gcmarkBits对应位为1——该对象不会在本次GC中被回收
  • 白色:对象未被标记,gcmarkBits对应位为0——该对象将会在本次GC中被回收

标记过程如下:

(1)起初所有的对象都是白色的;

(2)从根对象出发扫描所有可达对象,标记为灰色,放入待处理队列;

(3)从待处理队列中取出灰色对象,将其引用的对象标记为灰色并放入待处理队列中,自身标记为黑色;

(4)重复步骤(3),直到待处理队列为空,此时白色对象即为不可达的“垃圾”,回收白色对象;

其中,根对象包括全局变量、执行栈以及寄存器。

三色标记法的原理就是标记内存中那些还在使用中的部分,而内存中不再使用的部分,就是要回收的垃圾,需要将其回收,以供后续内存分配使用。Golang使用的垃圾回收机制是三色标记法配合写屏障和辅助GC,三色标记法是标记清除法的一种增强版本。

回收过程

GC工作的完整流程如下,Golang GC的大部分处理是和用户代码并行的:

  1. 初始化GC任务,包括开启写屏障和辅助GC,统计root对象的任务数量等。这个过程需要STW。
  2. 扫描所有root对象,包括全局指针和goroutine栈上的指针,将其加入标记队列,并循环处理灰色队列的对象,知道灰色队列为空,该过程后台并行执行。
写屏障
辅助GC
GC优化

GC性能是与对象数量有关的,对象越多GC性能越差,对程序的影响也越大。在开发中可以减少对象分配个数,采用对象复用、将小对象组合成大对象等方法进行优化。

GC触发条件

自动垃圾回收的触发条件有两个:

  • 超过内存大小阈值
  • 达到定时时间
GO的内存逃逸
指针逃逸

函数中创建了一个对象,函数完成后返回了这个对象的指针,在这种情况下如果我们就不能分配在stack上,因为stack上的对象会随着函数完成而回收,因此只能分配在堆上。

interface{} 动态类型逃逸

在go中,空接口就相当于抽象类,如果函数参数为interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。

声明切片的情况
1
2
n:=100
temp:=make([]int,n)

如上题所示,声明切片的容量的时候我们用的是变量n,在编译期间n是未知的,这个时候我们会将其分配到堆上,这也能解释为什么数组的声明不能使用变量。

闭包

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!