内存分区
代码经过预处理、编译、汇编、链接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合起来叫做静态区或全局区
- 代码区
存放 CPU 执⾏的机器指令。通常代码区是可共享的(即另外的执⾏程序可以调⽤它),使其可共享的⽬的是对于频繁被执⾏的程序,只需要在内存中有⼀份代码即可。代码区通常是只读的,使其只读的原因是防⽌程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
- 全局初始化数据区/静态数据区(data)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
- 未初始化数据区(bss)
存⼊的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执⾏之前被内核初始化为 0 或者空(nil)。
程序在加载到内存前,代码区和全局区(data和bss)的⼤⼩就是固定的,程序运⾏期间不能改变。
然后,运⾏可执⾏程序,系统把程序加载到内存,除了根据可执⾏程序的信息分出代码区
(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
- 栈区(stack)
栈是⼀种先进后出的内存结构,由编译器⾃动分配释放,存放函数的参数值、返回值、局部变量等。
在程序运⾏过程中实时加载和释放,因此,局部变量的⽣存周期为申请到释放该段栈空间。
- 堆区(heap)
堆是⼀个⼤容器,它的容量要远远⼤于栈,但没有栈那样先进后出的顺序。⽤于动态内存分配。堆在内存中位于BSS区和栈区之间。
根据语⾔的不同,如C语⾔、C++语⾔,⼀般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
Go语⾔、Java、python等都有垃圾回收机制(GC),⽤来⾃动释放内存。
Go Runtime内存分配
Go语⾔内置运⾏时(就是Runtime),抛弃了传统的内存分配⽅式,改为⾃主管理。这样可以⾃主地实现更好的内存使⽤模式,⽐如内存池、预分配等等。这样,不会每次内存分配都需要进⾏系统调⽤。
Golang运⾏时的内存分配算法主要源⾃ Google 为 C 语⾔开发的TCMalloc算法,全称Thread�Caching Malloc。
核⼼思想就是把内存分为多级管理,从⽽降低锁的粒度。它将可⽤的堆内存采⽤⼆级分配的⽅式进⾏管理。
每个线程都会⾃⾏维护⼀个独⽴的内存池,进⾏内存分配时优先从该内存池中分配,当内存池不⾜时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
- 基本策略
- 每次从操作系统申请⼀⼤块内存,以减少系统调⽤。
- 将申请的⼤块内存按照特定的⼤⼩预先的进⾏切分成⼩块,构成链表。
- 为对象分配内存时,只需从⼤⼩合适的链表提取⼀个⼩块即可。
- 回收对象内存时,将该⼩块内存重新归还到原链表,以便复⽤。
- 如果闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销。
注意:
内存分配器只管理内存块,并不关⼼对象状态,⽽且不会主动回收,垃圾回收机制在完成清理操作后,触发内存分配器的回收操作
- 内存管理单元
分配器将其管理的内存块分为两种:
- span:由多个连续的⻚(page [⼤⼩:8KB])组成的⼤块内存。
- object:将span按照特定⼤⼩切分成多个⼩块,每⼀个⼩块都可以存储对象。
⽤途:
span ⾯向内部管理
object ⾯向对象分配
//path:Go SDK/src/runtime/malloc.go
_PageShift = 13
_PageSize = 1 << _PageShift //8KB
在基本策略中讲到,Go在程序启动的时候,会先向操作系统申请⼀块内存,切成⼩块后⾃⼰进⾏管理。
申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB⼤⼩。
注意:这时还只是⼀段虚拟的地址空间,并不会真正地分配内存
- 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区域所有指针的⼤⼩。
- 内存管理组件
内存分配由内存分配器完成。分配器由3种组件构成:
- cache
每个运⾏期⼯作线程都会绑定⼀个cache,⽤于⽆锁 object 的分配 - central
为所有cache提供切分好的后备span资源 - heap
管理闲置span,需要时向操作系统申请内存
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
}
- 分配流程
- 计算待分配对象的规格(size_class)
- 从cache.alloc数组中找到规格相同的span
- 从span.manualFreeList链表提取可⽤object
- 如果span.manualFreeList为空,从central获取新的span
- 如果central.nonempty为空,从heap.free/freelarge获取,并切分成object链表
- 如果heap没有⼤⼩合适的span,向操作系统申请新的内存
- 释放流程
- 将标记为可回收的object交还给所属的span.freelist
- 该span被放回central,可以提供cache重新获取
- 如果span以全部回收object,将其交还给heap,以便重新分切复⽤
- 定期扫描heap⾥闲置的span,释放其占⽤的内存
注意:以上流程不包含⼤对象,它直接从heap分配和释放
- 总结
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)
- 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)的错误?
如果不是, 那么你是否发现程序越来越慢, 并最终死掉, 尽管仍然还有⾜够的内存可⽤?
并发:回收器是如何利⽤多核机器的?
伸缩:当堆内存变⼤时, 回收器该如何⼯作?
调优:回收器的默认使⽤或在进⾏调优时, 它的配置有多复杂?
预热时间:回收算法是否会根据已发⽣的⾏为进⾏⾃我调节?如果是, 需要多⻓时间?
⻚释放:回收算法会把未使⽤的内存释放回给操作系统吗?如果会, 会在什么时候发⽣?
- 三⾊标记
- 有⿊⽩灰三个集合,初始时所有对象都是⽩⾊
- 从Root对象开始标记, 将所有可达对象标记为灰⾊
- 从灰⾊对象集合取出对象, 将其引⽤的对象标记为灰⾊, 放⼊灰⾊集合, 并将⾃⼰标记为⿊⾊
- 重复第三步, 直到灰⾊集合为空, 即所有可达对象都被标记
- 标记结束后, 不可达的⽩⾊对象即为垃圾. 对内存进⾏迭代清扫,回收⽩⾊对象
- 重置GC状态
- 写屏障
三⾊标记需要维护不变性条件:
⿊⾊对象不能引⽤⽆法被灰⾊对象可达的⽩⾊对象。
并发标记时, 如果没有做正确性保障措施,可能会导致漏标记对象,导致实际上可达的对象被清扫掉。
为了解决这个问题,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
根据查看汇编可发现如下:
- 三⾊状态
并没有真正的三个集合来分别装三⾊对象。
前⾯分析内存的时候, 介绍了go的对象是分配在span中, span⾥还有⼀个字段是gcmarkBits, mark阶段⾥⾯每个bit代表⼀个slot已被标记.。
⽩⾊对象该bit为0, 灰⾊或⿊⾊为1. (runtime.markBits)
每个p中都有wbBuf和gcw gcWork,以及全局的workbuf标记队列,实现⽣产者-消费者模型, 在这些队列中的指针为灰⾊对象,表示已标记,待扫描。
从队列中出来并把其引⽤对象⼊队的为⿊⾊对象, 表示已标记,已扫描(runtime.scanobject)。
- 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