在多并发程序中,申请内存资源避免资源竞争,需要加锁机制。为了减少加锁,提高性能,go语言在内存分配中提供mcache,mcentral,mheap三个组件。
- mcache:管理线程本地缓存的mspan
- mcentral:管理全局的mspan供所有线程使用,mcache申请资源或者向mheap申请资源。
- mheap:管理Go的所有动态分配内存,及堆的内存管理。
mspan
mspan是内存管理的基本存储单元,有70多种mspan的规格大小。
mheap的管理
mheap主要管理内存页(8k),在堆内存中申请时,mheap就是把内存页组织起来形成对应的内存
内存栈管理栈管理主要是申请内存栈包括扩容压缩的方法
分段栈和连续栈
分段栈
Go1.3以前使用的分段栈,当栈空间不够时,会开辟新的栈空间不是连续的。使用的方法runtime.morestack和runtime.newstack。缺点:函数返回时会将新分配的内存释放回堆中,当有大量函数调用,比如递归,循环执行函数时会造成频繁创建,释放栈存储,这就是所谓的热分裂问题。
连续栈
当栈不够用时,开辟出更大的栈将原栈复制到新的栈空间中。步骤如下
- 调用runtime.newstack在内存中分配更大空间
- 使用runtime.copystack将旧栈中的内容复制
- 将指向旧栈对应变量的指针重新指向新站
- 调用runtime.stackfree销毁并回收旧栈的内存空间
栈的缩容
当栈区的空间使用率不超过1/4,那么在垃圾回收的时候使用runtime.shrinkstack进行栈缩容。
指令
// 观察栈的扩容缩容的过程
go build -gcflags -S main.go
垃圾回收
对于栈管理没有垃圾回收,函数执行完成自动释放。对于申请的大资源存放在堆中的,系统会定期把没有引用的资源清理掉。Go1.5之前使用标记清楚(Mark-And-Sweep)算法。v1.5使用三色标记清楚算法。v1.8使用浑河写屏障
GC算法的种类
标记清除算法
跟踪算法一种,会在那听应用程序执行,进行标记,没有标记到的自动回收。STW
三色标记清除算法
- 白色对象:潜在的垃圾,其内存可能会被垃圾收集器回收;
- 黑色对象:活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象,垃圾回收器不会扫描这些对象的子对象;
- 灰色对象:活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
步骤:
- 在进入 GC 的三色标记阶段的一开始,所有对象都是白色的。
- 遍历根节点集合里的所有根对象,把根对象引用的对象标记为灰色,从白色集合放入灰色集合。
- 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
- 重复第三步, 直到灰色集合中无任何对象。
- 回收白色集合里的所有对象,本次垃圾回收结束。
写屏障
避免执行标记算法有新的内存开辟,没有标记上,开启写屏障,把所有新增对象标记为灰色。
混合写屏障
在Go 语言 v1.7 版本之前,使用的是Dijkstra插入写屏障保证强三色不变性,但是运行时并没有在所有的垃圾收集根对象上开启插入写屏障。因为 Go 语言的应用程序可能包含成百上千的 goroutine,而垃圾收集的根对象一般包括全局变量和栈对象,如果运行时需要在几百个 goroutine 的栈上都开启写屏障,会带来巨大的额外开销,所以 Go 团队在实现上选择了在标记阶段完成时暂停程序、将所有栈对象标记为灰色并重新扫描,在活跃 goroutine 非常多的程序中,重新扫描的过程需要占用 10 ~ 100ms 的时间。
Go 语言在 v1.8 组合 Dijkstra 插入写屏障和 Yuasa 删除写屏障构成了如下所示的混合写屏障,该写屏障会将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色
为了移除栈的重扫描过程,除了引入混合写屏障之外,在垃圾收集的标记阶段,我们还需要将创建的所有新对象都标记成黑色,防止新分配的栈内存和堆内存中的对象被错误地回收,因为栈内存在标记阶段最终都会变为黑色,所以不再需要重新扫描栈空间
内存逃逸是减少栈空间转到堆空间耗费系统资源的分析方式。
造成内存逃逸的条件
- 返回的是指针或者引用,闭包函数使用外部变量
- 变量类型无法确定 interface{}
- 变量占用的大小过大
逃逸指令
go tool compile -m main.go
参考
内存分配
栈管理
垃圾回收
内存逃逸