一.Golang内存管理
我们先来对比下C与Golang的内存分配:
Golang内存分配特点:
- 预先从操作系统申请一大块内存。
- 内存分配算法采用Google的 TCMalloc算法,预先将申请的内存分成不同大小的内存集合,给不同场景的内存使用。
- 回收内存会放入内存池,并不会直接分配给操作系统。
介绍TCMalloc的几个重要概念
- Page:操作系统对内存管理以页为单位,大小为8KB。
- Span:一组连续的Page被称为Span,代码中是mspan,mspan是TCMalloc中内存管理的基本单位。
- mcache:每个p各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个协程有自己的ThreadCache,所以ThreadCache访问是无锁的。
- mcentral:mcentral是所有协程共享的缓存,也是保存的空闲内存块链表需加锁访问。
- mheap:mheap是对堆内存的抽象,mheap存了两棵树,链表保存的是Span。当CentralCache的内存不足时,会从PageHeap获取空闲的内存Span,然后把1个Span拆成若干内存块,添加到对应大小的链表中并分配内存;当CentralCache的内存过多时,会把空闲的内存块放回mheap中。
二.内存逃逸
内存逃逸的概念:
Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。变量被存储在堆上的过程叫做逃逸。
常见的逃逸类型:
- 申请过大的空间:make的大小设定的很大。
- 引用指针变量一定逃逸(返回值、参数,直接修改指针变量),一定会逃逸。
- Slice、Map、Channel里面有指针,一定会逃逸。
- 调用接口类型的方法,一定发生逃逸。
所以,我们经常讨论的一个问题,结构体是值传递还是指针传递?值传递会有内存拷贝,分配在栈区。指针传递会逃逸在栈区,虽然没有内存拷贝的过程,但GC压力会变大。如果使用很长的slice或map,还是用指针传递吧。
三.Golang的GC机制
- 首先创建三个集合:白、灰、黑。
- 将所有对象放入白色集合中。
- 然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。
- 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。
- 重复 4 直到灰色中无任何对象。
- 通过write-barrier检测对象有变化,重复以上操作。
- 收集所有白色对象(垃圾)。