Go(Golang)_13_垃圾处理器

Go_13_垃圾处理器

  • 垃圾处理器
    • 三色并发标记法
      • 强三色不变式
      • 弱三色不变式
      • 混合写屏障
    • 内存管理
      • 基础概念
      • 内存分配
      • 内存逃逸

垃圾处理器

垃圾处理器(Garbage Collection,GC):Go中实现的内存自动管理机制

1)Go通过三色并发标记法实现内存管理机制,其需通过STW;

2)STW(Stop The World):CPU全部用于执行内存回收(不执行代码);


三色并发标记法

三色并发标记法:通过三种不同颜色标记程序中资源对象的状态

1)基于标记-清楚算法(Mark And Sweep)实现;

2)三色并发标记法依赖STW,保证标记资源对象引用的关系正确性;

//若无STW会导致新创建的对象被当成垃圾回收或垃圾被当成新建对象


三色并发标记实现流程如下:

(1)将程序中所有资源对象均标记为“白色”;

1)程序指向的对象,代表该对象可达(该对象为可用对象);

2)程序不能通过其他对象间接指向的对象,代表该对象不可达(无用);
Go(Golang)_13_垃圾处理器_第1张图片


(2)GC从程序节点遍历所有对象,将遍历后的对象标记为“灰色”;

1)非递归遍历:仅遍历可直接到达的对象;
Go(Golang)_13_垃圾处理器_第2张图片


(3)GC从灰色对象开始遍历,将灰色对象标记为“黑色”;

1)遍历灰色对象的同时,将灰色对象可直达的对象标记为“灰色”;
Go(Golang)_13_垃圾处理器_第3张图片


(4)重复第(3)步,直到程序中无灰色对象;
Go(Golang)_13_垃圾处理器_第4张图片


(5)回收所有白色标记的对象(回收垃圾);
Go(Golang)_13_垃圾处理器_第5张图片


强三色不变式

强三色不变式:限制黑色对象不可指向白色对象

1)插入屏障:在黑色对象指向白色对象,强制将白色对象标记为灰色;

2)要对栈和堆进行两次三色并发标记,栈进行前需进行STW;

3)缺陷:STW会导致程序短时间不可用;



插入屏障实现流程如下:

(1)将程序中所有资源对象均标记为“白色”;

1)Go中仅能在堆中实现插入屏障;
Go(Golang)_13_垃圾处理器_第6张图片


(2)GC从程序节点遍历所有对象,将遍历后的对象标记为“灰色”;
Go(Golang)_13_垃圾处理器_第7张图片


(3)GC从灰色对象开始遍历,将灰色对象标记为“黑色”;
Go(Golang)_13_垃圾处理器_第8张图片


(4)此时程序在栈区添加对象9和对象8分别由对象1和对象4指向;
Go(Golang)_13_垃圾处理器_第9张图片


(5)重复第(3)步,直到程序中无灰色对象;
Go(Golang)_13_垃圾处理器_第10张图片


(6)重新对栈区进行三色并发标记,同时对栈区开启STW暂停保护;

1)开始STW保证在标记时不会有新的对象添加(不会丢失对象);
Go(Golang)_13_垃圾处理器_第11张图片


(7)回收所有白色标记的对象(回收垃圾);
Go(Golang)_13_垃圾处理器_第12张图片


弱三色不变式

弱三色不变式:限制黑色对象只能指向其他灰色对象直接/间接指向的白色对象

1)删除屏障:若删除的对象为灰色或白色,则强制标记为灰色;

2)缺陷:回收精度较低(GC运行之前,会启动STW对程序作快照);


删除屏障实现流程如下:

(1)将程序中所有资源对象均标记为“白色”;

1)Go中仅能在栈中实现删除屏障;
Go(Golang)_13_垃圾处理器_第13张图片


(2)GC从程序节点遍历所有对象,将遍历后的对象标记为“灰色”;
Go(Golang)_13_垃圾处理器_第14张图片


(3)当删除对象5的引用时将触发删除屏障,对象5将被标记为灰色;
Go(Golang)_13_垃圾处理器_第15张图片


(4)GC从灰色对象开始遍历,将灰色对象标记为“黑色”;
Go(Golang)_13_垃圾处理器_第16张图片


(5)重复第(4)步,直到程序中无灰色对象;
Go(Golang)_13_垃圾处理器_第17张图片


(6)回收所有白色标记的对象(回收垃圾);
Go(Golang)_13_垃圾处理器_第18张图片


混合写屏障

混合写屏障(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
Go(Golang)_13_垃圾处理器_第19张图片
//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申请内存的流程
Go(Golang)_13_垃圾处理器_第20张图片
//回收内存时会先将内存放到mcache中以便复用(过多时才归还部分内存给OS)


内存逃逸

内存逃逸:编译器将原分配到栈的内存分配到堆

1)栈内存回收策略:函数执行结束后自动回收;

2)堆内存回收策略:函数执行结束后交由GC处理;

3)本质:编译器根据对象是否被函数外部引用决定是否逃逸;


内存逃逸可实现:

1)返回函数的局部变量;

2)不同函数之间互相调用对方内部变量;


内存逃逸场景:

1)栈空间不足时;

2)函数外部调用局部变量时;

3)编译阶段无法确定参数类型时;

//内存逃逸在编译阶段就完成

你可能感兴趣的:(Go(Golang),互联网精神,golang,开发语言,后端)