垃圾处理器(Garbage Collection,GC):Go中实现的内存自动管理机制
1)Go通过三色并发标记法实现内存管理机制,其需通过STW;
2)STW(Stop The World):CPU全部用于执行内存回收(不执行代码);
三色并发标记法:通过三种不同颜色标记程序中资源对象的状态
1)基于标记-清楚算法(Mark And Sweep)实现;
2)三色并发标记法依赖STW,保证标记资源对象引用的关系正确性;
//若无STW会导致新创建的对象被当成垃圾回收或垃圾被当成新建对象
三色并发标记实现流程如下:
(1)将程序中所有资源对象均标记为“白色”;
1)程序指向的对象,代表该对象可达(该对象为可用对象);
2)程序不能通过其他对象间接指向的对象,代表该对象不可达(无用);
(2)GC从程序节点遍历所有对象,将遍历后的对象标记为“灰色”;
(3)GC从灰色对象开始遍历,将灰色对象标记为“黑色”;
1)遍历灰色对象的同时,将灰色对象可直达的对象标记为“灰色”;
强三色不变式:限制黑色对象不可指向白色对象
1)插入屏障:在黑色对象指向白色对象,强制将白色对象标记为灰色;
2)要对栈和堆进行两次三色并发标记,栈进行前需进行STW;
3)缺陷:STW会导致程序短时间不可用;
插入屏障实现流程如下:
(1)将程序中所有资源对象均标记为“白色”;
(2)GC从程序节点遍历所有对象,将遍历后的对象标记为“灰色”;
(4)此时程序在栈区添加对象9和对象8分别由对象1和对象4指向;
(6)重新对栈区进行三色并发标记,同时对栈区开启STW暂停保护;
1)开始STW保证在标记时不会有新的对象添加(不会丢失对象);
弱三色不变式:限制黑色对象只能指向其他灰色对象直接/间接指向的白色对象
1)删除屏障:若删除的对象为灰色或白色,则强制标记为灰色;
2)缺陷:回收精度较低(GC运行之前,会启动STW对程序作快照);
(1)将程序中所有资源对象均标记为“白色”;
(2)GC从程序节点遍历所有对象,将遍历后的对象标记为“灰色”;
(3)当删除对象5的引用时将触发删除屏障,对象5将被标记为灰色;
混合写屏障(Hybrid Write Barrier):结合插入屏障和删除屏障
1)优点:极大减少STW占用时间,同时提高回收精度;
//Go1.8之后均使用混合写屏障,且仅在堆中启用
混合写屏障遵循的三色并发标记遵循以下要求:
1)栈中的所有可达对象均标记为黑色;
2)在栈上创建的对象均被标记为黑色;
3)删除对象的引用时,均标记为灰色;
4)添加对象或对象的引用时,均标记为灰色;
//若对象已为黑色,则不能更改
内存管理:管理堆区内存的分配和释放
1)Go内存分配原理与Tcmalloc类似(在其基础上添加内存逃逸和GC)
2)Tcmalloc核心思想:将内存分隔较小块并多级管理(降低锁的粒度)
(1)page
1)说明:OS中内存分配的最小单位4KB(Go的page为8KB);
(2)arena
1)说明:由从OS中分配的内存形成程序所谓的“堆区”
2)堆区由多个page组成(page数 = 内存 / 8KB)
(3)span(内存管理的基础单元)
1)说明:存储特殊指针(每个指针指向arena中一个或多个page)
2)span将page又分为块管理,且指针大小默认为8byte
//64位系统下,arena为512GB、bitmap为512GB、span为512MB
runtime/mheap.go中span的数据结构定义(部分):
type mspan struct {
next *mspan // 链表的前向指针
prev *mspan // 链表的后向指针
// netx和prev形成双向链表(保证多个span的连接)
startAddr uintptr // 页的起始地址
npages uintptr // span管理的页数
nelems uintptr // 可用于分配块的个数
allocBits *gcBits // 分配位图
allocCount uint16 // 已分配块的个数
spanclass spanClass // class ID
elemsize uintptr // 块大小(class表中的对象大小)
(4)bitmap
1)说明:记录page中被使用的内存块(位图)
(5)class
1)说明:不同数量page组成的span(减少内存碎片)
2)class ID不同代表不同种类的span
Go中将所有划分为67种(0代表大于32KB的大对象)
class ID | bytes/obj | bytes/span | objects | waste bytes |
---|---|---|---|---|
1 | 8 | 8192 | 1024 | 0 |
2 | 16 | 8192 | 512 | 0 |
3 | 32 | 8192 | 256 | 0 |
4 | 48 | 8192 | 170 | 32 |
5 | 64 | 8192 | 128 | 0 |
6 | 80 | 8192 | 102 | 32 |
7 | 96 | 8192 | 85 | 32 |
8 | 112 | 8192 | 73 | 16 |
9 | 128 | 8192 | 64 | 0 |
10 | 144 | 8192 | 56 | 128 |
11 | 160 | 8192 | 51 | 32 |
12 | 176 | 8192 | 46 | 96 |
13 | 192 | 8192 | 42 | 128 |
14 | 208 | 8192 | 39 | 80 |
15 | 224 | 8192 | 36 | 128 |
16 | 240 | 8192 | 34 | 32 |
17 | 256 | 8192 | 32 | 0 |
18 | 288 | 8192 | 28 | 128 |
19 | 320 | 8192 | 25 | 192 |
20 | 352 | 8192 | 23 | 96 |
21 | 384 | 8192 | 21 | 128 |
22 | 416 | 8192 | 19 | 288 |
23 | 448 | 8192 | 18 | 128 |
24 | 480 | 8192 | 17 | 32 |
25 | 512 | 8192 | 16 | 0 |
26 | 576 | 8192 | 14 | 128 |
27 | 640 | 8192 | 12 | 512 |
28 | 704 | 8192 | 11 | 448 |
29 | 768 | 8192 | 10 | 512 |
30 | 896 | 8192 | 9 | 128 |
31 | 1024 | 8192 | 8 | 0 |
32 | 1152 | 8192 | 7 | 128 |
33 | 1280 | 8192 | 6 | 512 |
34 | 1408 | 16384 | 11 | 896 |
35 | 1536 | 8192 | 5 | 512 |
36 | 1792 | 16384 | 9 | 256 |
37 | 2048 | 8192 | 4 | 0 |
38 | 2304 | 16384 | 7 | 256 |
39 | 2688 | 8192 | 3 | 128 |
40 | 3072 | 24576 | 8 | 0 |
41 | 3200 | 16384 | 5 | 384 |
42 | 3456 | 24576 | 7 | 384 |
43 | 4096 | 8192 | 2 | 0 |
44 | 4864 | 24576 | 5 | 256 |
45 | 5376 | 16384 | 3 | 256 |
46 | 6144 | 24576 | 4 | 0 |
47 | 6528 | 32768 | 5 | 128 |
48 | 6784 | 40960 | 6 | 256 |
49 | 6912 | 49152 | 7 | 768 |
50 | 8192 | 8192 | 1 | 0 |
51 | 9742 | 57344 | 6 | 512 |
52 | 9728 | 49152 | 5 | 512 |
53 | 10240 | 40960 | 4 | 0 |
54 | 10880 | 32768 | 3 | 128 |
55 | 12288 | 24576 | 2 | 0 |
56 | 13568 | 40960 | 3 | 256 |
57 | 14336 | 57344 | 4 | 0 |
58 | 16384 | 16384 | 1 | 0 |
59 | 18432 | 73728 | 4 | 0 |
60 | 19072 | 57344 | 3 | 128 |
61 | 20480 | 40960 | 2 | 0 |
62 | 21760 | 65536 | 3 | 256 |
63 | 24576 | 24576 | 1 | 0 |
64 | 27264 | 81920 | 3 | 128 |
65 | 28672 | 57344 | 2 | 0 |
66 | 32768 | 32768 | 1 | 0 |
1)bytes/ojb:代表该对象的字节数
2)bytes/span:每个span占用堆的字节数(page数 * 8KB)
3)objcet:每个span可分配的对象个数(bytes/span / bytes/obj)
4)waste bytes:每个span产生的内存碎片(bytes/span % bytes/obj)
(6)mcache
1)说明:存储已被预分配给goroutine的不同种类span
2)每个P(协程调度器)都单独对应个mcache(实现无锁访问)
3)mcache在初始化时没任何span,使用过程中动态从mcentral获取并缓冲
runtime/mcache.go中mcache的数据结构定义(部分):
type mcache struct {
alloc [numSpanClasses]*mspan // 按class分组的mspan列表(指针数组)
1)alloc数组的大小为class总数的2倍;
2)数组中每个元素代表一种class类型的span列表;
3)每种class类型有两组span列表(对象是否包含指针);
//具有指针的对象被标为scan(GC只扫描scan对象,提高扫描性能)
(7)mcentral
1)说明:存储程序中所有种类的span
2)当mcache某个种类span被分配完时,会向mcentral申请该种类span;
runtime/mcentral.go中mcentral的数据结构定义:
type mcentral struct {
lock mutex // 互斥锁
spanclass spanClass // class ID
nonempty mSpanList // 有空闲块的span列表(双向链表)
empty mSpanList // 无空闲块的span列表(双向链表)
namlloc uint64 //已累计分配的对象个数
}
1)每个mcentral管理一组具有相同class ID的span列表;
2)申请span:特定种类span从nonempty链表中删除,并添加至empty链表;
3)归还span:特定种类span从empty链表中删除,并添加至nonempty链表;
//每次申请/归还操作都需获取锁(mcentral属于共享资源)
(8)mheap
1)说明:从OS申请内存页以形成并存储span
2)mcentral的span不足时会向mheap申请,mheap不足时会向OS申请
3)大于32KB的内存分配会直接从mheap处申请(略过mcache和mcentral)
runtime/mheap.go中mheap的数据结构定义(部分):
type mheap struct {
lock mutex // 互斥锁
spans []*mspan // 指向span(映射span和page的关系)
bitmap uintptr // bitmap的起始地址
arena_start uintptr // arena区域的首地址
arena_used uintptr // arena已使用区域的最大地址
central [numSpanClasses]struct { //每种class对应的两个mcentral
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
内存分配以对象大小分为以下3种分配策略:
对象大小分为 | 分配策略 |
---|---|
size < 16B | 小对象分配 (Tinny Allocations) |
16B <= size <= 32KB | 正常对象分配 (Small Allocations) |
size > 32KB | 大对象分配 (Large Allocations) |
//size < 16B时若对象包含指针,则使用正常对象分配
内存分配流程(正常对象分配):
1)获取当前goroutine的mcache;
2)计算对象所需内存大小size,并根据size计算出适合的class ID;
3)从mcache中的alloc[class]链表中查询是否有可用的span;
4)mcache不满足时,从mcentral申请个新的span加入mcache;
5)mcentral不满足是,从mheap申请个新的span加入mcentral;
7)mheap不满足时,向OS申请新的page(最小1M);
8)直至满足时,获取span中空闲对象的地址并返回;
//向mheap申请的span超出申请大小时,需进行页切分
//将多余页组成的新span添加到mheap的free列表中
如:内存管理下goroutine申请内存的流程
//回收内存时会先将内存放到mcache中以便复用(过多时才归还部分内存给OS)
内存逃逸:编译器将原分配到栈的内存分配到堆
1)栈内存回收策略:函数执行结束后自动回收;
2)堆内存回收策略:函数执行结束后交由GC处理;
3)本质:编译器根据对象是否被函数外部引用决定是否逃逸;
内存逃逸可实现:
1)返回函数的局部变量;
2)不同函数之间互相调用对方内部变量;
内存逃逸场景:
1)栈空间不足时;
2)函数外部调用局部变量时;
3)编译阶段无法确定参数类型时;
//内存逃逸在编译阶段就完成