Golang GC

内存分区

代码经过预处理、编译、汇编、链接4步后⽣成⼀个可执⾏程序。
在 Windows 下,程序是⼀个普通的可执⾏⽂件,以下列出⼀个⼆进制可执⾏⽂件的基本情况:

PS D:\Soft\GoCode\src> size .\01.exe
   text    data     bss     dec     hex filename
1440107   81844       0 1521951  17391f .\01.exe

由上可以得知,在没有运⾏程序前,也就是说程序没有加载到内存前,可执⾏程序内部已经
分好三段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分。
有些⼈直接把data和bss合起来叫做静态区或全局区

  1. 代码区

存放 CPU 执⾏的机器指令。通常代码区是可共享的(即另外的执⾏程序可以调⽤它),使其可共享的⽬的是对于频繁被执⾏的程序,只需要在内存中有⼀份代码即可。代码区通常是只读的,使其只读的原因是防⽌程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

  1. 全局初始化数据区/静态数据区(data)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

  1. 未初始化数据区(bss)

存⼊的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执⾏之前被内核初始化为 0 或者空(nil)。

程序在加载到内存前,代码区和全局区(data和bss)的⼤⼩就是固定的,程序运⾏期间不能改变。

然后,运⾏可执⾏程序,系统把程序加载到内存,除了根据可执⾏程序的信息分出代码区
(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。

  1. 栈区(stack)

栈是⼀种先进后出的内存结构,由编译器⾃动分配释放,存放函数的参数值、返回值、局部变量等。

在程序运⾏过程中实时加载和释放,因此,局部变量的⽣存周期为申请到释放该段栈空间。

  1. 堆区(heap)

堆是⼀个⼤容器,它的容量要远远⼤于栈,但没有栈那样先进后出的顺序。⽤于动态内存分配。堆在内存中位于BSS区和栈区之间。

根据语⾔的不同,如C语⾔、C++语⾔,⼀般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
Go语⾔、Java、python等都有垃圾回收机制(GC),⽤来⾃动释放内存。

1.jpg

Go Runtime内存分配

Go语⾔内置运⾏时(就是Runtime),抛弃了传统的内存分配⽅式,改为⾃主管理。这样可以⾃主地实现更好的内存使⽤模式,⽐如内存池、预分配等等。这样,不会每次内存分配都需要进⾏系统调⽤。

Golang运⾏时的内存分配算法主要源⾃ Google 为 C 语⾔开发的TCMalloc算法,全称Thread�Caching Malloc。

核⼼思想就是把内存分为多级管理,从⽽降低锁的粒度。它将可⽤的堆内存采⽤⼆级分配的⽅式进⾏管理。

每个线程都会⾃⾏维护⼀个独⽴的内存池,进⾏内存分配时优先从该内存池中分配,当内存池不⾜时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

  1. 基本策略
  • 每次从操作系统申请⼀⼤块内存,以减少系统调⽤。
  • 将申请的⼤块内存按照特定的⼤⼩预先的进⾏切分成⼩块,构成链表。
  • 为对象分配内存时,只需从⼤⼩合适的链表提取⼀个⼩块即可。
  • 回收对象内存时,将该⼩块内存重新归还到原链表,以便复⽤。
  • 如果闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销。

注意:内存分配器只管理内存块,并不关⼼对象状态,⽽且不会主动回收,垃圾回收机制在完成清理操作后,触发内存分配器的回收操作

  1. 内存管理单元

分配器将其管理的内存块分为两种:

  • span:由多个连续的⻚(page [⼤⼩:8KB])组成的⼤块内存。
  • object:将span按照特定⼤⼩切分成多个⼩块,每⼀个⼩块都可以存储对象。

⽤途:

span ⾯向内部管理

object ⾯向对象分配

//path:Go SDK/src/runtime/malloc.go
_PageShift = 13
_PageSize = 1 << _PageShift //8KB
arena.jpg

在基本策略中讲到,Go在程序启动的时候,会先向操作系统申请⼀块内存,切成⼩块后⾃⼰进⾏管理。

申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB⼤⼩。

注意:这时还只是⼀段虚拟的地址空间,并不会真正地分配内存

03mheap.png
  • arena区域

就是所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB⼤⼩的⻚,⼀些
⻚组合起来称为mspan。

//path:Go SDK/src/runtime/mheap.go
type mspan struct {
 next *mspan // 双向链表中 指向下⼀个
 prev *mspan // 双向链表中 指向前⼀个
 startAddr uintptr // 起始序号
 npages uintptr // 管理的⻚数
 manualFreeList gclinkptr // 待分配的 object 链表
 nelems uintptr // 块个数,表示有多少个块可供分配
 allocCount uint16 // 已分配块的个数
 ...
}
  • bitmap区域

标识arena区域哪些地址保存了对象,并且⽤4bit标志位表示对象是否包含指针、GC标记信
息。

  • spans区域

存放mspan的指针,每个指针对应⼀⻚,所以spans区域的⼤⼩就是
512GB/8KB*8B=512MB。
除以8KB是计算arena区域的⻚数,⽽最后乘以8是计算spans区域所有指针的⼤⼩。

  1. 内存管理组件

内存分配由内存分配器完成。分配器由3种组件构成:

  • cache
    每个运⾏期⼯作线程都会绑定⼀个cache,⽤于⽆锁 object 的分配
  • central
    为所有cache提供切分好的后备span资源
  • heap
    管理闲置span,需要时向操作系统申请内存
02go内存分配.png

3.1 cache

cache:每个⼯作线程都会绑定⼀个mcache,本地缓存可⽤的mspan资源。
这样就可以直接给Go Routine分配,因为不存在多个Go Routine竞争的情况,所以不会消耗锁资源。

mcache 的结构体定义:

//path:Go SDK/src/runtime/mcache.go
_NumSizeClasses = 67 //67
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96,
112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416,
448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,
1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528,
6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384,
18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
numSpanClasses = _NumSizeClasses << 1 //134
type mcache struct {
 alloc [numSpanClasses]*mspan //以numSpanClasses 为索引管理多个⽤于分配的
span
}

mcache⽤Span Classes作为索引管理多个⽤于分配的mspan,它包含所有规格的mspan。

它是 _NumSizeClasses 的2倍,也就是67*2=134,为什么有⼀个两倍的关系。

为了加速之后内存回收的速度,数组⾥⼀半的mspan中分配的对象不包含指针,另⼀半则包含指
针。对于⽆指针对象的mspan在进⾏垃圾回收的时候⽆需进⼀步扫描它是否引⽤了其他活跃的对
象。

3.2 central

central:为所有mcache提供切分好的mspan资源。

每个central保存⼀种特定⼤⼩的全局mspan列表,包括已分配出去的和未分配出去的。

每个mcentral对应⼀种mspan,⽽mspan的种类导致它分割的object⼤⼩不同。

//path:Go SDK/src/runtime/mcentral.go
type mcentral struct {
 lock mutex // 互斥锁
 sizeclass int32 // 规格
 nonempty mSpanList // 尚有空闲object的mspan链表
 empty mSpanList // 没有空闲object的mspan链表,或者是已被mcache取⾛的
msapn链表
 nmalloc uint64 // 已累计分配的对象个数
}

3.3 heap

heap:代表Go程序持有的所有堆空间,Go程序使⽤⼀个mheap的全局对象_mheap来管理堆内
存。

当mcentral没有空闲的mspan时,会向mheap申请。⽽mheap没有资源时,会向操作系统申请
新内存。mheap主要⽤于⼤对象的内存分配,以及管理未切割的mspan,⽤于给mcentral切割成⼩对象。

同时我们也看到,mheap中含有所有规格的mcentral,所以,当⼀个mcache从mcentral申请
mspan时,只需要在独⽴的mcentral中使⽤锁,并不会影响申请其他规格的mspan。

//path:Go SDK/src/runtime/mheap.go
type mheap struct {
 lock mutex
 spans []*mspan // spans: 指向mspans区域,⽤于映射mspan和page的关系
 bitmap uintptr // 指向bitmap⾸地址,bitmap是从⾼地址向低地址增⻓的
 arena_start uintptr // 指示arena区⾸地址
 arena_used uintptr // 指示arena区已使⽤地址位置
 arena_end uintptr // 指示arena区末地址
 central [numSpanClasses]struct {
 mcentral mcentral
 pad [sys.CacheLineSize�unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
 } //每个 central 对应⼀种 sizeclass
}
  1. 分配流程
  • 计算待分配对象的规格(size_class)
  • 从cache.alloc数组中找到规格相同的span
  • 从span.manualFreeList链表提取可⽤object
  • 如果span.manualFreeList为空,从central获取新的span
  • 如果central.nonempty为空,从heap.free/freelarge获取,并切分成object链表
  • 如果heap没有⼤⼩合适的span,向操作系统申请新的内存
  1. 释放流程
  • 将标记为可回收的object交还给所属的span.freelist
  • 该span被放回central,可以提供cache重新获取
  • 如果span以全部回收object,将其交还给heap,以便重新分切复⽤
  • 定期扫描heap⾥闲置的span,释放其占⽤的内存

注意:以上流程不包含⼤对象,它直接从heap分配和释放

  1. 总结
    Go语⾔的内存分配⾮常复杂,它的⼀个原则就是能复⽤的⼀定要复⽤。
  • Go在程序启动时,会向操作系统申请⼀⼤块内存,之后⾃⾏管理。
  • Go内存管理的基本单元是mspan,它由若⼲个⻚组成,每种mspan可以分配特定⼤⼩的
    object。
  • mcache, mcentral, mheap是Go内存管理的三⼤组件,层层递进。mcache管理线程在本地
    缓存的mspan;mcentral管理全局的mspan供所有线程使⽤;mheap管理Go的所有动态分配内存。
  • ⼀般⼩对象通过mspan分配内存;⼤对象则直接由mheap分配内存。

GC垃圾回收

Garbage Collection (GC)是⼀种⾃动管理内存的⽅式。⽀持GC的语⾔⽆需⼿动管理内存,程序后台⾃动判断对象。是否存活并回收其内存空间,使开发⼈员从内存管理上解脱出来。

垃圾回收机制

  • 引⽤计数
  • 标记清除
  • 三⾊标记
  • 分代收集

1959年, GC由 John McCarthy发明, ⽤于简化Lisp中的⼿动内存管理,到现在很多语⾔都提供了GC,不过GC的原理和基本算法都没有太⼤的改变 。

//C语⾔开辟和释放空间
int* p = (int*)malloc(sizeof(int));
//如果不释放会造成内存泄露
free(p);
//Go语⾔开辟内存空间
//采⽤垃圾回收 不要⼿动释放内存空间
p := new(int)
  1. Go GC发展
    Golang早期版本GC可能问题⽐较多,但每⼀个版本的发布都伴随着 GC 的改进
  • 1.5版本之后, Go的GC已经能满⾜⼤部分⼤部分⽣产环境使⽤要求
  • 1.8通过混合写⼊屏障, 使得STW降到了sub ms。 下⾯列出⼀些GC⽅⾯⽐较重⼤的改动
版本 发布时间 GC STW时间
v1.1 2013/5 STW 百ms-⼏百ms级别
v1.3 2014/6 Mark STW, Sweep 并⾏ 百ms级别
v1.5 2015/8 三⾊标记法, 并发标记清除 10ms级别
v1.8 2017/2 hybrid write barrier(混合写入屏障) sub ms

当前Go GC特征

三⾊标记, 并发标记和清扫,⾮分代,⾮紧缩,混合写屏障。

GC关⼼什么

程序吞吐量: 回收算法会在多⼤程度上拖慢程序? 可以通过GC占⽤的CPU与其他CPU时间的百分⽐
描述
GC吞吐量: 在给定的CPU时间内, 回收器可以回收多少垃圾?
堆内存开销: 回收器最少需要多少额外的内存开销?
停顿时间: 回收器会造成多⼤的停顿?
停顿频率: 回收器造成的停顿频率是怎样的?
停顿分布: 停顿有时候很⻓, 有时候很短? 还是选择⻓⼀点但保持⼀致的停顿时间?
分配性能: 新内存的分配是快, 慢还是⽆法预测?
压缩: 当堆内存⾥还有⼩块碎⽚化的内存可⽤时, 回收器是否仍然抛出内存不⾜(OOM)的错误?
如果不是, 那么你是否发现程序越来越慢, 并最终死掉, 尽管仍然还有⾜够的内存可⽤?
并发:回收器是如何利⽤多核机器的?
伸缩:当堆内存变⼤时, 回收器该如何⼯作?
调优:回收器的默认使⽤或在进⾏调优时, 它的配置有多复杂?
预热时间:回收算法是否会根据已发⽣的⾏为进⾏⾃我调节?如果是, 需要多⻓时间?
⻚释放:回收算法会把未使⽤的内存释放回给操作系统吗?如果会, 会在什么时候发⽣?
  1. 三⾊标记
  • 有⿊⽩灰三个集合,初始时所有对象都是⽩⾊
  • 从Root对象开始标记, 将所有可达对象标记为灰⾊
  • 从灰⾊对象集合取出对象, 将其引⽤的对象标记为灰⾊, 放⼊灰⾊集合, 并将⾃⼰标记为⿊⾊
  • 重复第三步, 直到灰⾊集合为空, 即所有可达对象都被标记
  • 标记结束后, 不可达的⽩⾊对象即为垃圾. 对内存进⾏迭代清扫,回收⽩⾊对象
  • 重置GC状态
4.png
  1. 写屏障
    三⾊标记需要维护不变性条件:
    ⿊⾊对象不能引⽤⽆法被灰⾊对象可达的⽩⾊对象。
    并发标记时, 如果没有做正确性保障措施,可能会导致漏标记对象,导致实际上可达的对象被清扫掉。

为了解决这个问题,go使⽤了写屏障。

写屏障是在写⼊指针前执⾏的⼀⼩段代码,⽤以防⽌并发标记时指针丢失,这段代码Go是在编译时加⼊的。

Golang写屏障在mark和mark termination阶段处于开启状态。

var obj1 *Object
var obj2 *Object
type Object struct {
 data interface{}
}
func (obj *Object) Demo() {
 //初始化
 obj1 = nil
 obj2 = obj
 //gc 垃圾回收开始⼯作
 //扫描对象 obj1 完成后
 //代码修改为:对象重新赋值
 obj1 = obj
 obj2 = nil
 //扫描对象 obj2
}
#将Go语⾔程序显示为汇编语⾔
go build -gcflags "-N -l"
go tool objdump -s 'main.Demo' -S ./Go程序.exe

根据查看汇编可发现如下:

5.jpg
6.jpg
  1. 三⾊状态

并没有真正的三个集合来分别装三⾊对象。

前⾯分析内存的时候, 介绍了go的对象是分配在span中, span⾥还有⼀个字段是gcmarkBits, mark阶段⾥⾯每个bit代表⼀个slot已被标记.。

⽩⾊对象该bit为0, 灰⾊或⿊⾊为1. (runtime.markBits)

每个p中都有wbBuf和gcw gcWork,以及全局的workbuf标记队列,实现⽣产者-消费者模型, 在这些队列中的指针为灰⾊对象,表示已标记,待扫描。

从队列中出来并把其引⽤对象⼊队的为⿊⾊对象, 表示已标记,已扫描(runtime.scanobject)。

  1. GC执行流程

GC 触发

  • gcTriggerHeap

分配内存时, 当前已分配内存与上⼀次GC结束时存活对象的内存达到某个⽐例时就触发GC。

  • gcTriggerTime:

sysmon检测2min内是否运⾏过GC, 没运⾏过 则执⾏GC。

  • gcTriggerAlways

runtime.GC()强制触发GC

5.1 启动

在为对象分配内存后,mallocgc函数会检查垃圾回收触发条件,并按照相关状态启动。

//path:Go SDK/src/runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer

垃圾回收默认是全并发模式运⾏,GC goroutine ⼀直循环执⾏,直到符合触发条件时被唤醒。

5.2 标记

并发标记分为两个步骤:

  • 扫描:遍历相关内存区域,依次按照指针标记找出灰⾊可达对象,加⼊队列。
//path:Go SDK/src/runtime/mgcmark.go
//扫描和对⽐bitmap区域信息找出合法指针,将其⽬标当作灰⾊可达对象添加到待处理队列
func markroot(gcw *gcWork, i uint32)
func scanblock(b0, n0 uintptr, ptrmask *uint8, gcw *gcWork)
  • 标记:将灰⾊对象从队列取出,将其引⽤对象标记为灰⾊,⾃身标记为⿊⾊。
//path:Go SDK/src/runtime/mgc.go
func gcBgMarkStartWorkers()

5.3 清理

清理的操作很简单,所有未标记的⽩⾊对象不再被引⽤,可以将其内存回收。

//path:Go SDK/src/runtime/mgcsweep.go
//并发清理本质就是⼀个死循环,被唤醒后开始执⾏清理任务,完成内存回收操作后,再次休眠,等待下次执⾏任务
var sweep sweepdata
// 并发清理状态
type sweepdata struct {
 lock mutex
 g *g
 parked bool
 started bool
 nbgsweep uint32
 npausesweep uint32
}
func bgsweep(c chan int)
func sweepone() uintptr
03mcache管理组件.png

发表评论

您的电子邮箱地址不会被公开。