golang 学习之路 — 内存分配器 - InfoQ 写作平台
全部标签
写点什么
创作场景
- 记录自己日常工作的实践、心得
- 发表对生活和职场的感悟
- 针对感兴趣的事件发表随笔或者杂谈
- 从 0 到 1 详细介绍你掌握的一门语言、一个技术,或者一个兴趣、爱好
- 或者,就直接把你的个人博客、公众号直接搬到这里
登录注册
开源云计算行业资讯玩转写作平台算法Python人工智能区块链Linux前端个人成长面试架构师编程企业动态新基建敏捷安全读书笔记高效工作团队管理创业活动专区生涯规划知识管理运维产品经理 查看更多
golang 学习之路 — 内存分配器
作者:en
2021 年 7 月 25 日
本文字数:3970 字
阅读完需:约 13 分钟
一. 前言
笔者在经过了前期基础学习后,用 go 语言来实现自己面临的业务问题已经不再是问题,所以拥有了另一方面的求知欲 —go 语言自身的各种包,各种机制是如何实现的,本章主要在探究 go 语言的内存分配器,希望能用本文讲清楚 go 语言内存分配器的机制,帮助大家更好地理解 go 语言的运行机制。
二. 简介
不同于 c 语言使用 malloc 和 free 来主动管理内存,golang 让程序员避免了这一切繁杂的操作,它通过 escape analysis 来分配内存,通过 garbage collection(gc)来回收内存,本文主要介绍内存分配部分的工作。
三. 详细解释
3.1 golang 内存分配时机
程序有两种内存,一种是堆栈 (stack),一种是堆(heap),所有的堆内数据都被 GC 管理。
我们要明白什么时候程序会分配内存,在某些语言中是程序员主动申请的,在 go 语言中则依赖 escape analysis,越多的值在堆栈,程序运行越快(存取速度比堆要快,仅次于直接位于 CPU 中的寄存器),以下是内存分配的一些时机
- goloang 只会把函数中确定不在函数结束后使用的变量放到堆栈,否则就会放到堆:一个值可能在构造该值的函数之后被引用 —> 变量上传
package main
复制代码
使用命令 go build -gcflags=”-m -l” 得到结果
./main.go:10:2: moved to heap: x
复制代码
编译器确定值太大而无法放入堆栈
编译器在编译的时候无法得知这个值的具体大小
ps: 将变量下传,变量还会留在堆栈中
type Reader interface{
复制代码
3.2 golang 内存分配方式
3.2.1 TCMalloc
学习 go 语言的内存分配方式之前,我们先来看看另一个内存分配器 —>TCMalloc,全称Thread-Caching Malloc
。
TCMalloc 有两个重要组成部分:线程内存(thread cache)和页堆 (page heap)
3.2.1.1 线程内存
每一个内存页都被分为多个固定分配大小规格的空闲列表(free list
) 用于减少碎片化。这样每一个线程都可以获得一个用于无锁分配小对象的缓存,这样可以让并行程序分配小对象(<=32KB)非常高效。
如图所示,第一行就是长度为 8 字节的内存块,在 thread cache 内最大的为 256 字节的内存块
3.2.1.2 页堆
TCMalloc 管理的堆由一组页组成(page 一般大小为 4kb),一组连续的页面被表示为 span。当分配的对象大于 32KB,将使用页堆(Page Heap)进行内存分配,分配对象时,大的对象直接分配 Span,小的对象从 Span 中分配。
当没有足够的空间分配小对象则会到页堆获取内存。如果页堆页没有足够的内存,则页堆会向操作系统申请更多的内存。
3.2.1.3 内存分配器
将基于 Page 的对象分配,和 Page 本身的管理串联
每种规格的对象,都从不同的 Span 进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个 CentralCache 内存,我们用链表把所有 Span 组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span。
3.2.1.3 总结
最终我们得到结构图如下:
TCMalloc 针对不同的对象分配采用了不同的形式
每个线程都一个线程局部的 ThreadCache,按照不同的规格,维护了对象的链表;如果 ThreadCache 的对象不够了,就从 CentralCache 进行批量分配;如果 CentralCache 依然没有,就从 PageHeap 申请 Span;如果 PageHeap 没有合适的 Page,就只能从操作系统申请了。
在释放内存的时候,ThreadCache 依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache 发现一个 Span 的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap 发现一批连续的 Page 都释放了,就可以归还给操作系统。
由此,TCMalloc 的核心思路即:
把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
3.2.2 Go 内存分配器结构
3.2.2.1 初始化
Go 在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。
申请到的内存块被分配了三个区域,在 X64 上分别是 512MB,16GB,512GB 大小。
arena 区域
就是我们所谓的堆区,Go 动态分配的内存都是在这个区域,它把内存分割成8KB
大小的页,一些页组合起来称为mspan
。
bitmap 区域
标识arena
区域哪些地址保存了对象,并且用4bit
标志位表示对象是否包含指针、GC
标记信息。bitmap
中一个byte
大小的内存对应arena
区域中 4 个指针大小(指针大小为 8B )的内存,所以bitmap
区域的大小是512GB/(4*8B)=16GB
。
spans 区域
存放mspan
(也就是一些arena
分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans
区域的大小就是512GB/8KB*8B=512MB
。除以 8KB 是计算arena
区域的页数,而最后乘以 8 是计算spans
区域所有指针的大小。创建mspan
的时候,按页填充对应的spans
区域,在回收object
时,根据地址很容易就能找到它所属的mspan
。
spans 区域
存放mspan
(也就是一些arena
分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans
区域的大小就是512GB/8KB*8B=512MB
。除以 8KB 是计算arena
区域的页数,而最后乘以 8 是计算spans
区域所有指针的大小。创建mspan
的时候,按页填充对应的spans
区域,在回收object
时,根据地址很容易就能找到它所属的mspan
。
go 初始化的时候会将内存页分为如下 67 个不同大小的内存块,最大到 32kb
3.2.2.2 结构及流程总览
Go 的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于 16B)、一般对象(大于 16B,小于等于 32KB)、大对象(大于 32KB)。
大体上的分配流程:
1.32KB 的对象,直接从 mheap 上分配;
2.<=16B 的对象使用 mcache 的 tiny 分配器分配;
3.(16B,32KB] 的对象,首先计算对象的规格大小,然后使用 mcache 中相应规格大小的 mspan 分配;
如果 mcache 没有相应规格大小的 mspan,则向 mcentral 申请
如果 mcentral 没有相应规格大小的 mspan,则向 mheap 申请
如果 mheap 中也没有合适大小的 mspan,则向操作系统申请
3.2.2.3 自底向上名词解释
3.2.2.3.1 内存管理单元
mspan:Go 中内存管理的基本单元,是由一片连续的 8KB 的页组成的大块内存。是一个包含起始地址、mspan 规格、页的数量等内容的双端链表, mspan 由一组连续的页组成,按照一定大小划分成object
。
结构图
3.2.2.3.2 内存管理元件
mcache:Go 像 TCMalloc 一样为每一个 逻辑处理器(P)(Logical Processors) 提供一个本地线程缓存(Local Thread Cache)称作 mcache,所以如果 Goroutine 需要内存可以直接从 mcache 中获取,由于在同一时间只有一个 Goroutine 运行在 逻辑处理器(P)(Logical Processors) 上,所以中间不需要任何锁的参与。
对于每一种大小规格都有两个类型:
- scan — 包含指针的对象。
- noscan — 不包含指针的对象。
采用这种方法的好处之一就是进行垃圾回收时 noscan 对象无需进一步扫描是否引用其他活跃的对象。
(<=16B 的对象使用 mcache 的 tiny 分配器分配)
结构体
//path: /usr/local/go/src/runtime/mcache.go
复制代码
结构图
central(mcentral):为所有mcache
提供切分好的mspan
资源。每个central
保存一种特定大小的全局mspan
列表,包括已分配出去的和未分配出去的。 每个mcentral
对应一种mspan
,而mspan
的种类导致它分割的object
大小不同。当工作线程的mcache
中没有合适(也就是特定大小的)的mspan
时就会从mcentral
获取。mcentral
被所有的工作线程共同享有,存在多个 Goroutine 竞争的情况,因此会消耗锁资源。
//path: /usr/local/go/src/runtime/mcentral.go
复制代码
结构图
mheap: 代表 Go 程序持有的所有堆空间,Go 程序使用一个mheap
的全局对象_mheap
来管理堆内存。
当mcentral
没有空闲的mspan
时,会向mheap
申请。而mheap
没有资源时,会向操作系统申请新内存。mheap
主要用于大对象的内存分配,以及管理未切割的mspan
,用于给mcentral
切割成小对象。
同时我们也看到,mheap
中含有所有规格的mcentral
,所以,当一个mcache
从mcentral
申请mspan
时,只需要在独立的mcentral
中使用锁,并不会影响申请其他规格的mspan
。
//path: /usr/local/go/src/runtime/mheap.go
复制代码
结构图:
arena:golang 中所有堆区的统称,以 x64 为例子就是 512GB 的虚拟地址空间。
四. 之后目标
1.go 语言的垃圾回收
进程调度,线程调度,协程调度
虚拟内存
五. 参考学习
https://www.youtube.com/watch?v=ZMZpH4yT7M0
https://www.linuxzen.com/go-memory-allocator-visual-guide.html
https://zhuanlan.zhihu.com/p/29216091
https://zhuanlan.zhihu.com/p/59125443
划线
评论
复制
发布于: 2021 年 07 月 25 日阅读数: 645
版权声明: 本文为 InfoQ 作者【en】的原创文章。
原文链接:【https://xie.infoq.cn/article/e760c46349bd7b443e38ac332】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
en
关注
努力分享对他人有价值的知识 2018.06.14 加入
还未添加个人简介
评论 (2 条评论)
发布
xumc
深度好文
2021 年 07 月 26 日 09:22
0 __回复
en
感谢认可~
2021 年 07 月 26 日 10:11
0 __回复
没有更多了
促进软件开发及相关领域知识与创新的传播
商务专区联系我们
内容投稿:editors@geekbang.com
业务合作:hezuo@geekbang.com
反馈投诉:feedback@geekbang.com
加入我们:zhaopin@geekbang.com
联系电话:010-64738142
地址:北京市朝阳区叶青大厦北园InfoQ 近期会议
QCon 全球软件开发大会 10 月 21-23 日
ArchSummit 全球架构师峰会 11 月 12-13 日
GMTC 全球大前端技术大会 11 月 19-20 日
AICon 全球人工智能与机器学习技术大会 11 月 25-26 日
PCon 全球产品创新大会 11 月 26-27 日
DIVE 全球基础软件创新大会 2022 年 3 月 25-26 日
ArchSummit 全球架构师峰会 12 月 3-4 日
Copyright © 2021, Geekbang Technology Ltd. All rights reserved. 极客邦控股(北京)有限公司 | 京 ICP 备 16027448 号 - 5
京公网安备 11010502039052 号