

[译] Go语言内存管理与分配
source link: http://www.pengrl.com/p/38720/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文基于Go 1.13
Go程序的内存从申请阶段到不再使用后的释放阶段都由Go标准库自动管理。尽管管理工作不需要开发者参与,但是Go对内存管理的底层实现做了非常好的优化,里面充满了有意思的知识点,还是值得我们学习的。
从堆上申请内存
Go内存管理的设计目标是在并发环境下保持高性能,并且集成垃圾回收器。让我们从一个简单的例子开始:
package main type smallStruct struct { a, b int64 c, d float64 } func main() { smallAllocation() } //go:noinline func smallAllocation() *smallStruct { return &smallStruct{} }
//go:noinline
这行注释可以禁止编译时的内联优化,从而避免编译时把 smallAllocation
这个函数调用直接优化没了。
运行逃逸分析命令 go tool compile "-m" main.go
,得到内存申请情况:
main.go:14:9: &smallStruct literal escapes to heap
运行 go tool compile -S main.go
命令,获取程序的汇编代码,可以更清晰的查看内存申请情况:
0x001d 00029 (main.go:14) LEAQ type."".smallStruct(SB), AX 0x0024 00036 (main.go:14) PCDATA $0, $0 0x0024 00036 (main.go:14) MOVQ AX, (SP) 0x0028 00040 (main.go:14) CALL runtime.newobject(SB)
newobject
是用于申请内存的内建函数, newobject
是 mallocgc
的代理, mallocgc
是管理堆内存的函数。Go分配内存有两种策略:小块内存申请和大块内存申请。
小块内存申请
对于32KB以下的小块内存申请,Go会尝试从本地缓存 mcache
中获取内存。 mcache
包含了一系列被称为 mspan
的 span
列表, mspan
包含了可供分配使用的内存:
Go的线程调度模型中,每个系统线程 M
和一个上下文 P
挂钩,在一个指定时间点最多只能处理一个协程 G
。申请内存时,当前协程会首先在所属 M
的本地缓存中的 span
列表中查找可用的内存块。使用本地缓存的好处是不用加锁,更高效。
span
列表按大小被划分为大约70个等级,大小从8字节到32K字节不等,不同等级存储不同大小的内存块:
在我们前面的例子,结构体的大小为32字节,所以使用32字节的 span
:
每个等级的 span
链表会存在两份:一个链表用于存储内部不包含指针的对象,另一个链表用于存储内部包含指针的对象。这么的好处是垃圾回收时更高效,因为不需要扫描不包含指针的那个 span
链表。
译者 yoko 注:
对英文原文做个补充。
每个 mcache
包含了 2 * 67
个链表(一个元素个数为 2 * 67
的数组,数组中的一个元素即为一个 mspan
链表)。
这里的67怎么来的呢,为什么不是1呢?
实际上每个 mspan
都各自管理了一大块内存块,而每个 mspan
又被切割成n个小内存块( object
), object
才是真正分配给用户使用的内存块。
那么问题来了, mspan
按多大切割成 object
合适呢,太小可能不满足用户申请的大小,太大又造成浪费。
Go采取的策略是将32K大小以内的大小预定义了67个大小等级,每一个链表中的所有 mspan
都按该链表所设定的大小等级切割 object
。
这样,用户申请内存时,向上取最接近的大小等级,然后去对应的链表中的 mspan
获取可用的 object
。
英文原文关于这部分说的不太清楚,并且上面的两张图画得都不是太准确。实际上应该是一行可能有多个 mspan
,然后每个 mspan
内又可能包含多个 object
。
现在,你可能会奇怪如果 mcache
上没有空闲的内存块可供分配该怎么办。Go另外还维护了全局的 span
列表,同样也按大小分成多个级别,叫做 mcentral
。 mcentral
包含两种链表,一张包含空闲内存块,一张包含已使用内存块:
mcentral
维护了两张 span
链表。一张链表为 non-empty
类型,包含了可供分配的 span
(由于一个 span
可能包含多个 object
,只要有一个或一个以上的object可供分配即表示该 span
可供分配),一张为 empty
类型,包含已分配完毕的 span
。当Go执行垃圾回收时,如果 span
中的内存块被标记为可供分配, span
会重新加入到 non-empty
链表中。
从 mcentral
获取 span
的流程图如下:
当 mcentral
中也没有可供分配的 span
时,Go会从堆上申请新的 span
并将其放入 mcentral
中:
堆在必要时向操作系统申请内存。它会申请一块大内存,被称为 arena
,在64位系统下为64MB,其它大部分系统为4MB,申请的内存同样用 span
管理:
大块内存申请
Go申请大于32KB的大块内存不使用本地缓存策略,而是将大小取整到页大小整数倍后直接从堆上申请。
全局图
现在我们在一个较高层次上,对Go的内存分配有了一个大致了解。让我们将所有的组件集合到一起来绘制一张全局图:
设计灵感
Go内存分配器的设计基于TCMalloc,TCMalloc是由Google专门为并行环境优化的内存分配器。TCMalloc的 文档 很值得一读,在文档里你也能找到本文中讲解到的一些概念。
原文链接: https://pengrl.com/p/38720/
原文出处: yoko blog ( https://pengrl.com )
原文作者:yoko
版权声明:本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。
Recommend
-
69
-
44
原文: medium.com/a-journey-w… 这...
-
10
:information_source: 这篇文章基于 Go 1.13。 在内存从分配到回收的生命周期中,内存不再被使用的时候,标准库会自动执行...
-
15
在C语言程序开发中,提到动态内存分配时,基本上每个程序员都明白 calloc() 和 malloc() 库函数的区别——calloc() 函数不仅分配内存,还会将分配后的内存清零,而 malloc() 函数则对分配好的内存不做任何操作。calloc() 函数的效率比 malloc()+memset()...
-
12
By KSkun, 2021/11实习时接触了许多关于游戏开发相关的内存话题,在这里分享一些基于 Unity 游戏开发视角可能用得上的底层知识。这个系列将分成 4 期来探讨以下 4 个话题:内存分配与管理内存占用、泄露的排查方法PC、And...
-
17
PHP内存管理ZMM(三)-内存分配函数emalloc 2018-04-11 emalloc是ZMM中heap层实现的函数,其内部调用_zend_mm_alloc_int函数。在_zend_mm_alloc_int中会依次在heap层的缓存区、小内存区、...
-
1
深入理解 Go | 内存管理:内存分配 发表于 2020-04-20...
-
6
GC 只管理生命期,不管理具体的内存分配2022-08-01读到一篇很好玩的文章 treadmill gc。 这应该是我读过的最简单的一篇能实现 non-...
-
7
理解Linux内存管理:分配、释放和管理内存 作者:编程技术汇 2023-10-18 09:30:45 系统 Linux内存管理负责分配、释放和管理内存资源,采用虚...
-
7
内存管理,是开发者在程序编写和调优的过程中不可绕开的话题,也是走向资深程序员必须要了解的计算机知识。有经验的面试官会从内存管理的掌握程度去考察一个候选人的技术水平,这里面涉及到的知识可能包括操作系统、计算机组成原理以及编程语言的底层实现等。...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK