# golang 内存分配和 gc 垃圾回收
# 相关基本概念
# 栈上内存分配
- 函数调用完成,会自动释放内存(所以不存在,内存分配和垃圾)
# 堆分配
- 在c语言里面返回一个函数内部变量的指针是存在问题的
- 函数栈针销毁后,这个地址是非法的
- 应该将变量放到堆里面才可以
- golang可以这么干,是做了逃逸分析
# 逃逸分析
- golang 会自动的将应该放在堆上的变量放在堆里
go build -gcflags="-m" main.go显示分析过程,会打印逃逸分析
# 内存管理的三个角色
Mutator就是程序员写的应用程序,不断修改对象引用关系Allocator内存分配器,负责管理从操作系获取内存- c语言 malloc 底层有内存分配器的实现(glibc)
- tcmalloc 是 malloc 多线程改进版
- 另外 golang 使用的实现类似 tcmalloc
Collector垃圾收集器,负责清理死对象,释放内存空间
# 进程虚拟内存的布局
- 栈内存从高地址向低地址增长
- 堆地址从低地址向搞地质增长
- 当堆地址的顶部和栈顶碰撞,则会 OOM (out of memory)
- 多线程的时候,主线程在(相对)最高地址乡下
- 每个线程会维护一段小的栈
- 多个线程共享一个堆内存
# Allocator 内存分配器
# 两种类型的分配器
- 线性分配器,使用很少(被free的内存就浪费了)
- 空闲链表分配器
- 将free的内存用链表链接起来,再分配内存的时候优先考虑链表上的内存
- 分配算法
Fist-Fit从头便利链表,第一个满足要求的内存块Next-Fit使用环形链表,上一次分配成功的内存地址开始便利链表Best-Fit从头便利,使用要分配内存最匹配的内存块Segregated-Fit内存分级,先把内存按照常用大小分块,再分配
# c内存分配
- malloc,判断需要内存是否大于128k
- 小于,brk 系统调用 则调整堆空间顶位置,会产生堆内存增长
- 大于,mmap 系统调用 任意未使用内存分配
- 空闲链表分配器
# go语言内存分配相关
- 新版本使用稀疏堆向os申请 比如64M一块一块的内存空间
- 使用 mmpa 系统调用申请
- 每使用玩一个64M块就使用 mmap 系统调用申请 新的一块64 MB虚拟内存
- 列表保存每块虚拟内存的开始和结束位置
# 分配器再go中维护一个多级结构
mcache -> mcentral -> mheap- mcache, 与 P 绑定,本地内存分配操作,不需要加锁
- mcenteral, 中心分配缓存,分配时需要上锁, 不同 spanClass 使用不同的锁(小力度锁)
- spanCLass分类,就是内存拆成不同大小的块(66种大概, 8bytes 到 32 KB)
- mheap, 全局唯一,从 OS 申请内存,修改定义结构时需要加锁,全局锁。
# 1 分配大小分类
Tinysize < 16 bytes && 没有指针- 每个 P 绑定 mcache ,它包含 tiny指针, tinyoffset等。
- 首先从 tiny 和 tiny offset里面找
- tiny 指针指向一个 elem ,一个elem可以存放多个微对象。
- 每次申请小对象就往elem里面塞,直到塞不进去为止
- 如果elem满了, mcache.alloc 就会从mcenteral 获取内存,
- mcenteral:种类有 spanClass * 2 个
- 奇数位 nospan 类型
- 偶数位 span 类型
- mcenteral:种类有 spanClass * 2 个
- mcachem没有时会触发 mcache.Refill流程:
- 尝试从mcenter 的 non-empty链表查找(mcenteral.cacheSpan)
- 尝试sweep mcentral 的 empty, 插入到 non-empty
- 增长mcentral,尝试从 arena 获取内存(mcentral.grow)
- arena 内存可以理解为 go 预先申请的那 64 MB 内存
- arena 如果还没有,就会向 os 申请, (mheap.alloc)
Small有指针对象 || 16 bytes <= size <= 32 KB- 相对简单,秩序要去 mcenter 里面找对应大小为止就可以了
- 没有了就去全局拿
Largesize > 32 KB- 直接越过 mcache、mcentral、直接从 mheap 进行相应数量 page 分配
- 大内存分配改过几个版本了简单说就是 空闲链表 转化为 尽量高度低的树。
- 这里 go 向操作系统索要内存就不一定是 64 MB为单位了
# golang GC 垃圾回收
# 垃圾分类(哈哈哈)
- 语义垃圾(内存泄漏)
- 逻辑上没用到,但是其实还被应用着,GC 不会认为它是垃圾,无能为力(程序bug)
- 比如:
arr:= make([]MyStructOnHeap, 5) // 这里后面的内存不会被释放, 就是 arr = arr[:2]1
2
3 - 语法垃圾:
- 应用程序无法到达的垃圾,GC的主要管理目标
# mark 标记流程
三色抽象, 会把对象分为3类
- 黑:已经扫描完成,子节点扫描完毕 gcmarkbits = 1 在队列外
- 白:已经扫描完成子节点未扫描完毕 gcmarkbits = 1 在队列内
- 灰:未扫描 collector 不知道任何相关信息
从root开始扫描,root可能是全局变量,栈上指针 。。
广度优先遍历算法,将元素入队列,标记灰色,其子节点都入队列后,标记为黑,并移除队列
当遍历完成所有有用的对象被标记为黑色,剩下的白色对象被标记为垃圾
# 垃圾回收代码流程
- gcStart 启动所有p后台的 gcMarkWorker 以及所有根节点 gcRootPrepare,并使gcMarkWorker进入休眠状态
- schedule 调度流程里面有一个 findRunnableGCWorker,会唤醒合适数量的的gcMarkWorker
- gcMarkWork 广度优先遍历 对象,设置标灰,并放入 gcwork 的队列里面
- gcMarkWork 调用 gcMarkDown 排空 wbBuf,就会唤醒后台沉睡 清理,和还内存给os的协程。
- 实战gc优化关注点
- 因为gc标记主要是 广度优先算法,抓住重点减少gc数量
- 多做复用
- 减少对象数量
- 因为gc标记主要是 广度优先算法,抓住重点减少gc数量