分享
  1. 首页
  2. 文章

Golang GC

强某某 · · 1659 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

内存分区

代码经过预处理、编译、汇编、链接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

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:强某某

查看原文:Golang GC

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1659 次点击 ∙ 1 赞
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏