tyltr技术窝

内存管理与垃圾回收#

在谈go内存管理和回收的时候,我们需要知道这主要指的是对 堆内存 的管理与回收!千万不要混淆栈和堆!

c/c++的堆内存管理是由开发者控制的,也就是说需要人为的申请、释放内存空间,这种动态内存的生命周期有程序员决定。例如使用 malloc分配堆内存,free释放内存空间。go语言抛弃了c/c++这种方式,而是采用主动申请与主动释放,增加了逃逸分析和GC,将开发者从内存管理中释放出来,让开发者有更多的精力去关注软件设计,而不是底层的内存问题。这种设计确实解放的程序员的工作量,但是对于go这种号称高性能的语言来说,却是一种“负担”。

golang 垃圾回收机制一直被诟病并且一直在改进。目前是采用的方式是“三色标记 + 写屏障”的方式。三色标记作为“标记-清除”算法的一种,存在着“标记-清除”算法的缺点 STW 问题( stop the world )。这些都是老生常谈了,那么下面咱们系统地说明这些问题。

1.内存#

内存是啥?你确定你了解内存吗?理解内存对go的内存管理与垃圾回收很重要!下面我们会从虚拟内存、栈和堆,以及堆内存的管理等作出阐述。

1.1 物理内存与虚拟内存#

内存,作为计算机存储器的重要组成部分之一。在访问者看来,内存是由M个字节组成的数组,每个字节都有一个唯一的地址(这个地址就是物理地址)。他的访问方式和数组相同,第一个地址为0,依次是2,3,4,……M-1。也就是说,它是一个物理连续的空间,一旦知道地址就可以快速的取出内容,这种访问就是物理寻址。如图所示:

物理寻址示意图

在计算机发展早期,这种物理寻址的方式很常见。但现在的计算机普遍采用的是虚拟寻址。

2.内存管理#

2.1 栈内存与堆内存#

内存其实也是分‘区域’的。如:内核虚拟空间、栈内存、堆内存等等
那么在内存回收方面,栈内存与堆内存的不同:

  • 栈内存:由程序自动向操作系统申请分配以及回收,速度快,使用方便,但程序员无法控制。若分配失败,则提示栈溢出错误

  • 堆内存:程序员向操作系统申请一块内存,分配的速度较慢,地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则则导致内存泄露。

也就是说,分配在栈内存的变量不需要人为的干预,分配在堆内存的变量是可以人为干预的。换一种说法,进行适合的人为干预,可以对程序的性能进行提升,相反则可能影响程序的性能。

2.2 go如何实现内存管理的#

由上面阐述,可以知道 c/c++malloc 申请的变量分配到堆内存上。那么go是不是也分配到堆内存上呢?不完全是。go有可能把变量分配堆内存上,也有可能分配到栈内存上。那么什么情况下会把变量分配到堆内存、什么情况分配到栈内存呢?

变量存储在堆上还是栈上由内部实现决定,与语法是无关的。Golang编译器决定变量应该分配到什么地方时会进行逃逸分析(逃逸分析是编译器用来确定由程序创建的值所处位置的过程)。具体来说,编译器执行静态代码分析,以确定是否可以将值放在构造函数的栈(帧)上,或者该值是否必须“逃逸”到堆上。

如果可能的话,golang编译器会函数的局部变量分配到栈内存上,但对于非常大的局部变量也会被分配到堆内存上。对于一些变量,编译器无法确定在函数return之后还会不会被利用的话,这些变量会被分配到堆内存上。

参考官方文档

2.3.1 tcmalloc#

golang的内存管理是基于tcmalloc的。不妨先了解一下 tcmalloc。

tcmalloc是google推出的一种内存分配器,常见的内存分配器还有glibc的ptmalloc和google的jemalloc。相比于ptmalloc,tcmalloc性能更好,特别适用于高并发场景。

tcmalloc分配的内存主要来自两个地方:全局缓存堆和进程的私有缓存。

对于一些小容量的内存申请试用进程的私有缓存,私有缓存不足的时候可以再从全局缓存申请一部分作为私有缓存。对于大容量的内存申请则需要从全局缓存中进行申请。而大小容量的边界就是32k。缓存的组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。

2.3.2 go的内存分配#

todo:go的内存分配过程,稍后说明

3.垃圾回收#

由几个问题开始下面的话题。

问题一:什么是垃圾?

程序在运行过程中,那些曾经申请在堆上存储,并且后续不再使用的对象,就是垃圾,我们需要把它所占用的内存释放掉。

问题二:谁来回收垃圾?

垃圾是在堆内存中存储的,而堆内存实际由用户程序进行管理的,垃圾自然由用户程序进行回收,操作系统不会“帮忙”回收。如果用户程序不进行垃圾回收,那么堆内存空间迟早会被消耗一空。

问题三:回收垃圾容易实现吗?

垃圾回收的算法有很多,无论哪种算法都需要解决两个问题

  • 如何判断某对象是否是垃圾
  • 对垃圾的回收

3.1 业内常见的垃圾回收算法#

业内常见的垃圾回收算法

  • 引用计数

原理:每一个对象都维护一个引用计数,每增加一个对此对象的引用,引用计数加1;减少引用,引用计数减1。当对象的引用计数为0的时候,也就意味着该对象没有被引用了,可以被视为垃圾,进行回收。
语言:python
存在的缺陷: 循环引用问题。也就是两个无用的对象,相互引用,造成两个对象的引用计数不能为0,不可进行回收。

  • 标记-清除

原理:首先将用户程序挂起;然后将所有的对象,进行标记(即进行判断是否是垃圾,使得垃圾与其他对象进行区分);再然后进行清除垃圾。这样可以解决引用计数的循环引用问题
语言golangpython
存在的缺陷:用户程序挂起,也就是STW问题(stop the world),虽然go一直在优化GC,但也只是尽量缩短STW的时间。目前尚没有方法决绝STW问题。

  • 分代回收

原理:认为,存在时间长的对象被继续使用的可能性要大一些,而存在时间短的继续被使用的可能性要小一些。根据这种思想,按照对象生命周期长短划分不同的代,不同代有不能的回收算法和回收频率(被回收可能性的代回收频率越高,理论上说生命周期越长的对象被回收频率越低)
语言java
存在的缺陷:算法实现复杂

3.2 go的垃圾回收#

go的垃圾回收是采用的三色标记法+写屏障的方式。三色标记法是“标记-清除”算法的一种。

3.2.1 三色标记#

3.2.2 写屏障#

todo 未完待续。。。

扩展推荐#