GoLang 内存管理

2019-02-20 language golang

在 C 语言中,通过 malloc() 函数可以动态分配内存,常用的有 ptmalloc2(glib)、tcmalloc(Google)、jemaloc(FaceBook),后两者在避免内存碎片以及性能上都有较大的优势。

而 Go 中的内存分配器,其原理与 tcmalloc 类似,简单的说就是维护一块大的全局内存,每个线程 (P) 维护一块小的私有内存,私有内存不足再从全局申请。

简介

TCMalloc 算法的全称为 Thread-Caching Malloc ,其核心思想就是把内存分为多级管理,从而降低锁的粒度。

初始化

在 Go 程序启动的时候会先申请一块虚拟内存 (未真正分配物理内存),然后会切割成小块自己管理,在 64 位的机器上,内容如下。

+-------------+---------------+----------------------------+
| spans(512M) |  bitmap(16G)  |        arena(512G)         |
+-------------+---------------+----------------------------+

其中,arena 区域就是所谓的堆区,动态分配的内存都是在这个区域,会把内存分成 8KB 大小的页,mspan

bitmap 区域顾名思义,就是标示 arena 中指针大小,总体大小为 512G/(4*8B)=16G

spans 区域保存的是 arena 内存管理数据结构 mspan 的指针,其大小为 512GB/8KB*8B=512MB

逃逸分析

在 C/C++ 中,所有的内存都是统一管理,在调用 malloc 或者 new 函数时,会在堆上分配一块内存空间,而且程序员需要负责释放资源,否则就会发生内存泄漏。

而 Go 语言中,通过 new 分配的内存不一定在堆上,而且会通过 GC 自动回收内存。

简介

所谓的逃逸分析,就是编译器执行静态代码分析后,对内存管理进行的优化和简化。

在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,就称这个指针发生了逃逸。

通过逃逸分析的结果,决定一个变量到底是分配在堆上还是分配在栈上。

在堆上的内存需要通过 GC 进行回收,其使用效率要比栈上的内存慢很多。

测试

通过 -gcflags 相关的参数,可以判断变量是否发生了逃逸。

package main

import "fmt"

func foo() *int {
        t := 3
        return &t
}

func main() {
        x := foo()
        fmt.Println(*x)
}

使用如下命令进行测试,其中 -l 是为了防止发生内联。

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:7:9: &t escapes to heap
./main.go:6:2: moved to heap: t
./main.go:12:14: *x escapes to heap
./main.go:12:13: main ... argument does not escape

可以看到 foo() 函数中 t 变量发生了逃逸,而且 x 也同样发生了逃逸。

后者是因为有些函数参数为 interface 类型,比如 fmt.Println(a ...interface{}) ,那么编译期间很难确定其参数的具体类型,也会发生逃逸。

另外,也可以通过 go tool compile -S main.go 反汇编,查看是否存在 runtime.newobject 的调用。

Garbage Collection

当前 Go 使用的垃圾回收机制是三色标记法配合写屏障和辅助 GC,而其中的三色标记法是标记-清除法的一种增强版本。

如下是 Go 中 GC 算法的里程碑:

v1.1 Stop The World, STW
v1.3 Mark STW, Sweep 并行
v1.5 三色标记法
v1.8 hybrid write barrier

经典的 GC 算法有:A) 引用计数 (Reference Counting);B) 标记-清除 (Mark and Sweep);C) 复制收集 (Copy and Collection)。

如上,Go 中使用的是标记-清除的改进版本。

三色标记清除算法

原始的标记-清除算法包含了两步:A) 找出可达对象,并标记;B) 回收未标记对象。在做垃圾回收的过程中,需要停止程序的运行,也就会导致程序卡顿。