原文地址:https://www.douyacun.com/article/e8dc63c56bf36d90a86ba538d0360023
版本:
我是学习go源码时看到go内存分配使用tcmalloc,早在看redis源码时就了解到tcmalloc了,redis支持zmalloc、tcmalloc、jemalloc几种内存分配,之前只是看了redis自带的zmalloc,没深入了解一下tcmalloc。看来存在必有其道理,不定啥时候就用到
学习内存分配,就要先了解一下物理内存的相关概念和描述:
看完这些以后总结了几个问题:
什么是逻辑地址、线性地址、物理地址,linux是如何进行地址映射的?
linux是管理内存的(NUMA), 节点(node)、区域(zone)、page(页面)?
为什么一个16G的内存,内核需要160M维护page_table?
什么是缺页异常?(给顶的逻辑地址不合法,逻辑页面没有对应的物理页面,MMU将会产生中断,向核心发出信号)
进程如何分配内存:
自旋锁:多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的 - 摘自维基百科
基本思想: 小内存通过线程层次缓存分配(无锁,速度快),分配失败后请求上层补一批,前面分配了过多的内存则回收
线程:
ThreadCache: 每个线程私有一份,尺寸小于256K的小内存申请均由ThreadCache进行分配,线程之间不需要竞争,非常高效。节省了加锁释放锁的时间。
全局:
PageHeap: 中央堆处理器,被所有线程共享(分配需要锁定),负责和操作系统申请、释放内存,大尺寸内存申请直接通过PageHeap分配,
CentralCache: 作为PageHeap和ThreadCache的中间人,负责将PageHeap的内存切分为小块,恰当时机分配给ThreadCache,回收ThreadCache的内存并部分返还给PageHeap
span: 一块连续的内存页。 object: object是由span切分成的小块,object预设了一些规则,如8byte、16byte、32bbyte..., 同一个span切出来的object都是相同的规格,object不大于256k,超大内存直接分配span使用\
ThreadCache、CentralCache管理object, PageHeap管理的是span
大对象(>32k)对齐内存页面4k(4的倍数), 由central page heap处理,central page heap也是一个数组
k个pages分配过程:
// 根据起始page的位置和长度确定span的范围
struct Span {
PageID start; // Starting page number
Length length; // Number of pages in span
Span* next; // 注意,这里span结构一个双向链表
Span* prev; //
union {
void* objects; // Span会在CentralFreeList中拆分成由object组成的free list
char span_iter_space[sizeof(SpanSet::iterator)];
};
unsigned int refcount : 16; // span的object被引用次数,refcount = 0时表示此span没有被使用
unsigned int sizeclass : 8; // span被切分的object属于哪个级别的sizeClass
unsigned int location : 2; // Span在的位置IN_USE?normal?returned?
unsigned int sample : 1; // Sampled jobject?
bool has_span_iter : 1;
// What freelist the span is on: IN_USE if on none, or normal or returned
enum { IN_USE, ON_NORMAL_FREELIST, ON_RETURNED_FREELIST };
};
refcount、sizeclass、objects数据CentralFreeList管理的内存
PageHeap维护了
PageHeap结构体
class PERFTOOLS_DLL_DECL PageHeap {
PageMap pagemap_;
struct SpanList {
Span normal;
Span returned;
};
SpanSet large_normal_;
SpanSet large_returned_;
SpanList free_[kMaxPages];
};
CentralCache是由size_class个CentralFreeList组成的数组由Static(static_vars.h)类管理
class CentralFreeList {
SpinLock lock_;
// We keep linked lists of empty and non-empty spans.
size_t size_class_; // size-class span切分尺寸,每个span只能按同一种size-class切分
Span empty_; // 未分配Span
Span nonempty_; // 已分配Span
}
CentralFreeList作为中间人,从PageHeap中取出部分span并按照预定大小将其拆分为大小固定的object供ThreadCache共享。CentralFreelist是全局的,除了构造函数,其余操作都需加锁。
小对象分配
thread_cache free_list为空
central_cache free_list为空
每个thread独立维护了各自的离散式空闲列表
// https://github.com/gperftools/gperftools/blob/master/src/thread_cache.h
// https://github.com/gperftools/gperftools/blob/master/src/thread_cache.cc
class ThreadCache {
class FreeList {
void* list_; // 链表第一个节点
uint32_t length_; // 当前长度
int32_t size_;
}
ThreadCache* next_;
ThreadCache* prev_;
// kClassSizesMax 96
FreeList list_[96]; // size_class 数组,可用链表
int32 size_; // 可用内存大小
int32 max_size_; // size_ > max_size_ --> Scavenge()
pthread_t tid_; // 属于哪个线程
}
ThreadCache list_变量即为size class的实现,在实现free list时没有使用next来指针指向下一个位置,而是直接使用 void* list_, 将下一个object的地址直接存储在上个前8个字节中,可以模拟单向链表,object分配个应用程序后可以直接覆盖前8个字节,节省了一个指针的空间。
// 1. Sizes <= 1024 have an alignment >= 8. ceil(size/8)
// 2. Sizes > 1024 have an alignment >= 128. ceil(size/128).
// Size Expression Index
// -------------------------------------------------------
// 0 (0 + 7) / 8 0
// 1 (1 + 7) / 8 1
// ...
// 1024 (1024 + 7) / 8 128
// 1025 (1025 + 127 + (120<<7)) / 128 129
// ...
// 32768 (32768 + 127 + (120<<7)) / 128 376
go在程序启动时会分配一块虚拟内存地址是连续的内存, 结构如下
是page和其管理对象的反查表,在回收时,从哪来回哪去。利用该表可访问地址相邻大块内存使用状态,以便将多个相邻内存合并成更大的内存,减少碎片,更好适应分配需求。
和gc有关,先不做过多深入,研究gc时候会深入理解
栈(stack)、全局变量和静态变量(.data/.bss)占用1bit,0: gc不需要关心是否回收,1: 需要分析是否需要回收
堆(heap) 需要占用2bit
用户对象分配区域,就是通常说的堆(heap),go从heap中申请的内存就是在这个区域。
什么时候从heap分配对象,go会自动确定哪些对象应该放在栈上,哪些应该放在堆上。简单的说:当一个对象的内容可能在生成该对象的函数结束后被访问,那么这个对象就会被分配在堆上。
返回对象指针
func foo() *string {
str := "Hello world"
return &str
}
func main() {
fmt.Println(foo())
}
$ git:(master) ✗ go run -gcflags '-m -l' bar.go
# command-line-arguments
./bar.go:7:9: &str escapes to heap
./bar.go:6:2: moved to heap: str
./bar.go:11:17: foo() escapes to heap
./bar.go:11:13: main ... argument does not escape
0xc0000101e0
传递了对象的指针到其他函数
func greeter(name *string) {
*name = "hello " + *name
}
func main() {
name := "douyacun"
greeter(&name)
fmt.Println(name)
}
➜ bar git:(master) ✗ go run -gcflags '-m -l' bar.go
# command-line-arguments
./bar.go:6:20: "hello " + *name escapes to heap
./bar.go:5:14: greeter name does not escape
./bar.go:12:13: name escapes to heap
./bar.go:11:10: main &name does not escape
./bar.go:12:13: main ... argument does not escape
hello douyacun
在闭包中使用了对象并且需要修改对象
func main() {
name := "douyacun"
greeter := func() {
name = "hello " + name
}
greeter()
fmt.Println(name)
}
➜ bar git:(master) ✗ go run -gcflags '-m -l' bar.go
# command-line-arguments
./bar.go:11:13: name escapes to heap
./bar.go:8:19: "hello " + name escapes to heap
./bar.go:7:13: main func literal does not escape
./bar.go:11:13: main ... argument does not escape
hello douyacun
使用new
func main() {
name := new([]string)
fmt.Println(name)
}
➜ bar git:(master) ✗ go run -gcflags '-m -l' bar.go
# command-line-arguments
./bar.go:7:13: name escapes to heap
./bar.go:6:13: new([]string) escapes to heap
./bar.go:7:13: main ... argument does not escape
&[]
这里翻译了一下源代码的注释,描述的很清楚runtime/mheap.go
mcache分配
P是协程中的用于运行go代码的虚拟资源,同一时间只能有一个线程访问同一个P,所以p的资源不需要锁
mcentral分配
mheap分配
小对象分3个阶段获取可用的span,然后从span中分配对象
分配大内存直接通过mhe
ap获取,绕过mcache/mcentral, 大对象分配时会四舍五入(8kb),在包含k个页面的空闲列表中寻找第K个条目,如果他是空的,向上查找,直到找到,如果失败,从操作系统查看早