物理内存与虚拟内存
0. CPU与内存内存的最小存储单位为一个字节,我们通过一个十六进制的数据表示每个字节的编号(比如
4G
内存的十六进制表示从
0x0000 0000
到
0xffff ffff
)。其中内存地址的编号上限由地址总线
address bus
的位数相关,
CPU
通过地址总线来表示向内存说明想要存储数据的地址。
以位
32
为例,
CPU
包含
CPU
个针脚来传递地址信息,每个针脚能传递的信息为
32
(即
1bit
),对应的地址空间大小为
0 | 1
。
2^32 = 4G
同其他存储介质相比,内存的存储单元采用了随机读取存储器
RAM, Random Access Memory
,存储器读取数据花费的时间和数据所在的位置无关(而磁盘和磁带只能顺序访问数据,有损于速度和效率)。这个特性使得系统可以把控进程的运行时间,这也是内存称为主存储器的关键因素。内存的缺点是不能持久化数据,计算机一旦断电内存中的数据就会消失。
1. 早期的内存分配方式
在一开始是没有虚拟内存的概念的,程序访问的地址就是真实的物理地址,
CPU
可以直接根据寄存器中的值去访问对应的物理内存。计算机按照每个程序需要的内存大小进行分配,这种做法虽然看上去比较直观,但是存在如下几个问题:
- 程序员需要额外关心内存布局:假设内存总大小为
,第一个程序使用了n
数量的内存空间后,第二个程序使用的内存范围即为m
,需要妥善处理内存偏移m~n
- 内存使用效率低下:假如系统没有足够的空间运行新的程序时,需要选择一个已运行的程序拷贝到硬盘上释放出内存空间供新的程序使用,这种多余的
操作会降低计算机的运行效率IO
- 安全风险较高:不同进程的地址空间不隔离,一方面难以阻止恶意程序修改别的进程内存数据,另一方面某个程序的
也可能会污染其他进程的内存数据bug
2. 虚拟内存
虚拟内存在程序访问真实的物理地址之间封装了一层,每个进程的内存读写都是建立在属于该进程的虚拟内存上的,而虚拟内存映射到物理内存的工作统一交给操作系统处理。程序员既不需要担心不同进程的内存地址冲突的问题,又不需要关心硬件层面上虚拟内存是如何映射到物理内存的。
这种映射带来的好处包括:
- 以程序员的视角来看,每个程序申请的虚拟内存都是从 开始的连续空间,既不需要再去手动维护内存地址的偏移,也不需要担心不同程序之间的内存地址冲突
- 突破了物理内存大小的限制:操作系统分配给每个进程的虚拟进程可以比实际的物理内存大得多
- 虚拟内存中的连续空间不需要映射到物理内存中的连续空间,可以有效利用内存碎片
3. 进程与虚拟地址的关系
进程:程序编译后的可执行文件是放在磁盘上的,执行时首先将进程加载到内存中然后再放到寄存器中,最后让
Linux
执行程序,一个静态的程序就变成了进程。
CPU
进程在运行的时候相关数据需要存储在内存中,但是出于数据隔离性和安全性的考虑进程不能通过
0x 0000 0001
的方式直接访问物理内存,只能通过虚拟地址访问。以
Linux
为例,每个进程都有属于自己的
4GB
虚拟内存空间,虚拟地址到物理地址的翻译和不同进程的物理地址隔离是交给操作系统来实现的。
虚拟地址的实现禁止了进程空间访问物理内存地址,统一交给操作系统进行管理,这带来几个好处:
- 对于每个线程独享的数据,操作系统只需要将每个进程对应的虚拟空间地址映射到不同的物理空间地址就可以保证两个进程的执行互不干扰
- 对于需要”内存共享“的数据实现起来也很简单:只需要把多个进程的虚拟内存空间对应到同一物理内存空间即可(比如内存和共享库的实现)。
4. 分页
分页的核心思想是可执行文件的虚拟进程空间分成大小相同的多页,没执行到的页暂时保留在硬盘上,执行到哪一页就为该页在物理地址空间中分配内存,同时建立虚拟地址和物理地址页的映射关系。
尽管在真实的物理地址上封装了一层虚拟地址提高了程序设计的效率和安全性,但是操作系统需要额外耗费资源把进程的虚拟地址翻译成物理地址。在虚拟地址映射到物理地址的过程中,有几个问题需要考虑到:
- 为了高效地“翻译”虚拟地址,这个映射关系必须加载在内存中
- 程序在运行的时候具有局部性特点:在程序运行的某个具体的时间点只有小部分的数据会被访问到
- 如果虚拟地址中每个字节都维护一个对应记录的话,那么光是对应关系就已经耗费完内存的空间了
基于上述事实,
Linux
引入“分页”
paging
的方式来记录对应关系,即使用更大尺寸(一般为
4KB
)的单位
page
来管理内存,
Linux
中物理内存和进程虚拟内存都被分割成页。
paging
具有如下特点:
- 一页之内的地址是连续的:这样虚拟页和物理页一旦联系到一起,就意味着页内的顺序可以按照顺序一一对应起来
- 可以极大减少虚拟内存和物理内存的对应关系,如果页的大小是
,那么需要维护的对应关系就减少为4KB
- 由于
是4096
的2
次方,因此地址最后12
位可以同时表示虚拟地址和物理地址的偏移量12
,即该字节在页内的位置,地址的前一部分表示页编号,操作系统就只需要维护虚拟地址和物理地址的页编号对应关系offset
图源: https://www. cnblogs.com/vamei/p/932 9278.html
当进程需要读写内存时,首先唤醒
MMU
根据虚拟空间地址的页编号映射到真实的物理页,再根据偏移量找到实际的物理地址进行读写。如果程序尝试去访问一个没有映射到物理页的虚拟地址时(这种情况被称为缺页中断),操作系统会建立虚拟地址和物理地址的映射关系到页表中,如果物理内存不够用时操作系统会覆盖掉某个页(被覆盖的页如果曾经被修改过,那么需要将此页写回磁盘)。
Q:从这个角度说,在运行程序的时候,磁盘上可能存储着除可执行文件本身外的数据内容,即 4G
虚拟内存的一部分?
虚拟内存分布
我们编写的程序都运行在计算机的内存(一般指的是计算机的随机存储器
RAM
),
UNIX
环境内存的大致分布如下:
当应用程序被加载到内存空间中执行时,操作系统负责代码段、数据段和 BSS
段的加载,并在内存中为这些段分配空间。在程序运行期间,栈段也交给操作系统进行管理,只有堆段是程序员自行管理(可显式地申请和释放空间)的内存区域。
1. 各个内存段的内容
- 代码段
:存放程序执行代码的一段内存区域,这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(不过某些架构也允许修改程序)。代码段中的指令包含操作码和被操作对象(或对象地址引用)。只读常量将直接包含在代码段中;局部变量会在栈区分配内存空间并引用;全局变量会引用Text Segment
段和数据段中的地址。BSS
- 数据段
:静态内存分配,存放静态变量、初始化的全局变量和常量。Data Segment
-
段:静态内存分配,包含了程序中未初始化的全部变量,在内存中BSS
段全部置零。BSS
- 堆
:动态内存分配,当进程调用heap
等函数分配内存时,堆会扩张用于存放新申请的变量,使用malloc
等函数释放内存时,被释放的内存从堆中剔除。free
- 栈
:存放程序的局部变量,即在函数括弧stack
中定义的变量。除此之外,在函数被调用时参数会被压入栈中,函数调用结束后函数的返回值也会被存放在栈中。另外,栈具有先进先出的特点,经常用于保存/恢复调用现场,这也是我们在程序{}
时打印crash
的原因。stack
2. 汇编基础
回顾一下基本的汇编指令:
-
:将当前指令的下一条指令的地址保存到栈中;跳转至目标函数的地址call
-
:弹出栈顶地址将数据放入ret
eip
-
:从栈顶入栈push
-
:从栈底出栈pop
-
:将数据从源地址传送到目标地址mov
再回顾一下寄存器:
由于的运算速度远高于内存的读写速度,为了提高效率
CPU
本身自带一级和二级缓存。不过由于数据在一二级缓存中的地址是不固定的,
CPU
每次读写都要寻址也会拖慢速度,因此
CPU
使用寄存器存储最频繁读写的数据(比如循环变量)。每一个寄存器都有自己的名称,程序员直接告诉
CPU
去哪个寄存器拿数据,这样的速度是最快的。
CPU
- 一般寄存器
、AX
、BX
和CX
:分别表示累加寄存器、基址寄存器、计数寄存器和数据寄存器。DX
- 索引寄存器
、DI
:分别表示目的索引寄存器和来源索引寄存器。SI
-
:栈顶指针SP
-
:栈底指针BP
3. 栈调用实例
以
C++
为例,看一下这个小程序:
void foo (int n) {
return;
}
void bar (int n) {
int a = n * 2;
foo(a);
return;
}
int main() {
// ...
bar(10);
// ...
return 0;
}
在
https://gcc.godbolt.org/
查看汇编代码:
foo(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
nop
pop rbp
ret
bar(int):
push rbp
mov rbp, rsp
sub rsp, 24
mov DWORD PTR [rbp-20], edi
mov eax, DWORD PTR [rbp-20]
add eax, eax
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
mov edi, eax
call foo(int)
nop
leave
ret
main:
push rbp
mov rbp, rsp
mov edi, 10
call bar(int)
mov eax, 0
pop rbp
ret
按照约定,程序从
main
标签开始执行,然后开始执行前两行代码:
push rbp
mov rbp, rsp
BP
和
SP
都是堆栈的操作寄存器,其中
SP
一直指向栈顶(由于栈是往低地址方向增长的,执行
push
指令入栈时
SP
自动减少,执行
pop
指令出栈时
SP
自动增加),无法暂借使用,我们一般用
EBP
存取堆栈。
push rbp
将当前的
rbp
入栈,
mov rbp, rsp
将
rsp
的值赋给
rbp
,这样就可以通过
rbp + ?
来访问函数的参数,以
rbp - ?
访问函数的变量。
// 将10存放在edi寄存器中
mov edi, 10
// 程序寻找bar(int)的标签, 并为该函数建立一个新的栈帧
call bar(int)
跳转到
bar(int)
函数后开始执行该函数内的代码:
// 为函数的局部变量保存一些内存, 栈会增长
sub rsp, 24
// 将之前edi中存储的10写到地址[rbp-20]中
mov DWORD PTR [rbp-20], edi
// 实现*2的操作
mov eax, DWORD PTR [rbp-20]
add eax, eax
// 将bar()的返回值入栈
mov DWORD PTR [rbp-4], eax
// 将返回值20写入edi, 并调用foo(int)函数
mov eax, DWORD PTR [rbp-4]
mov edi, eax
call foo(int)
同样为
foo(int)
新建一个栈帧,并跳转到该函数:
// 将edi中的值入栈, 然而后面并没有用到
mov DWORD PTR [rbp-4], edi
// 空语句
nop
// rbp出栈, rbp恢复调用前的值
pop rbp
// 弹出返回地址, 程序返回到调用函数处并执行下一行指令
ret
这时程序继续执行
bar(int)
剩下的指令:
nop
// leave相当于mov esp, ebp; pop ebp
leave
ret
4. 为什么需要分区
最主要的还是每个区都有自己的特性,可以实现不同的功能:
- 全局变量和静态变量在程序的整个生命周期都可以被访问到,因此需要静态存储在一个内存区域;临时变量使用完之外没有存在的意义,需要及时回收以高效利用内存
- 栈维护一个后进先出的队列,性能优越并且不会产生内存碎片,但是对于没有价值的变量不能及时释放,会造成内存一定程度的浪费。堆允许程序员自行管理内存,极大提高了管理变量的自由度,但是如果忘记释放内存可能造成内存泄漏。
内存分配算法
1. 线性分配
线性分配算法是最简单的内存分配算法,通过一个指针指向空闲内存的首地址。当用户程序申请内存时,根据用户申请内存的大小返回指针后面相应大小的区域,并更新指针。这种方法有两个卓越的优点:
- 实现成本低
- 分配速度快
但这种简单粗暴的算法也有显著的缺点,比如当已分配内存的对象
2
成为垃圾对象时,我们无法回收对象
2
的内存用于下一个对象的分配。
2. 空闲链表分配
空闲链表
Free-List
会用一个链表存储所有的空闲内存,当用户程序申请内存时,扫描空闲内存链表并取出大小满足要求的内存块。已分配内存的对象成为垃圾对象时,对应的内存块会重新插入到空闲链表中。
一般而言空闲链表中有很多的内存块都可以满足用户程序申请内存块的大小要求,依据选取策略的不同我们将空闲链表分配划分为如下几种:
- 首先匹配
:从空闲链表头部开始扫描,返回第一个满足大小的空闲内存块First-Fit
- 最佳匹配
:遍历空闲链表,返回满足大小的最小空闲内存块Best-Fit
- 最差匹配
:遍历空闲链表,如果找不到正好符合大小的空闲内存块,从最大的空闲内存块中分配Worst-Fit
- 隔离匹配
:将空闲内存分为多个存储固定大小的链表,首先根据用户程序申请的内存大小确定合适的链表,再从链表中获取空闲内存块Segregated-Fit
隔离匹配综合了其他匹配方法的优势,减少了对空闲内存块的遍历,
Golang
的内存分配策略也是基于该匹配方法实现的。隔离匹配会将内存分割成
4B
、
8B
、
16B
和
32B
等多个链表,比如当我们向内存分配器申请
8B
的内存时,内存分配器会从第二个链表中找到空闲的内存块。
Golang内存管理前身:tcmalloc
1. 简介
tcmalloc
全称为
thread-caching malloc
,是
google
推出的一种内存管理器。按照对象所占内存空间的大小,
tcmalloc
将对象划分为三类:
- 小对象:
- 中对象:
- 大对象:
相关的重要概念:
-
:Page
将虚拟地址空间划分为多个大小相同的页tcmalloc
(大小为Page
)8KB
-
:单个Span
可能包含一个或多个Span
,是Page
向操作系统申请内存的基本单位tcmalloc
-
:对于Size Class
以内的小对象,256KB
按照大小划分了不同的tcmalloc
,比如Size Class
字节、8
字节和16
字节等,以此类推。应用程序申请小对象需要的内存时,32
会将申请的内存向上取整到某个tcmalloc
的大小Size Class
-
:每个线程自己维护的缓存,里面对于每个ThreadCache
都有一个单独的Size Class
,缓存了FreeList
个还未被应用程序使用的空闲对象,由于不同线程的n
是相互独立的,因此小对象从ThreadCache
中的ThreadCache
中存储或者回收时是不需要加锁的,提升了执行效率FreeList
-
:同样对于每个CentralCache
都维护了一个Size Class
来缓存空闲对象,作为各个Central Free List
获取空闲对象,每个线程从ThreadCache
中取用或者回收对象是需要加锁的,为了平摊加锁解锁的时间开销,一般一次会取用或者回收多个空闲对象CentralCache
-
:当PageHeap
中的空闲对象不够用时会向Centralache
申请一块内存然后再拆分成各个PageHeap
添加到对应的Size Class
中。CentralFreeist
对不同内存块PageHeap
的大小采用了不同的缓存策略:Span
以内的128 Page
每个大小都用一个链表来缓存,超过Span
的128 Page
存储在一个有序Span
中set
2. 小对象分配
对于小于等于
256KB
的小对象,分配流程如下:
- 将对象大小向上取整到对应的
Size Class
- 如果
非空则直接移除FreeList
第一个空闲对象并返回,分配结束FreeList
- 如果
为空,从FreeList
中该CentralCache
对应的SizeClass
加锁一次性获取一堆空闲对象(如果CentralFreeList
也为空的则向CentralFreeList
申请一个PageHeap
拆分成Span
对应大小的空闲对象,放入Size Class
中),将这堆对象(除第一个对象外)放到CentralFreeList
中ThreadCache
对应的Size Class
,返回第一个对象,分配结束FreeList
回收流程如下:
- 应用程序调用
或者free()
触发回收delete
- 将该对象对应的内存插入到
中该ThreadCache
对应的Size Class
中FreeList
- 仅当满足一定条件时,
中的空闲对象才会回到ThreadCache
中作为线程间的公共缓存CentralCache
3. 中对象分配
对于超过
256KB
但又不超过
1MB
(即
128 Pages
)的中对象,分配流程如下:
- 将该对象所需要的内存向上取整到
个k(k <=128)
,因此最多会产生Page
的内存碎片8KB
- 从
中的PageHeap
的链表中开始按顺序找到一个非空的链表(假如是k pages list
),取出这个非空链表中的一个n pages list, n>=k
并拆分成span
和k pages
的两个n-k pages
,前者作为分配结果返回,后者插入到span
n-k pages list
- 如果一直找到
都没找到非空链表,则把这次分配当成大对象分配128 pages list
4. 大对象分配
对于超过
128 pages
的大对象,分配策略如下:
- 将该对象所需要的内存向上取整到
个k
page
- 搜索
,找到不小于large span set
个k
的最小page
(假如是span
),将该n pages
拆成span
和k pages
的两个n-k pages
,前者作为结果返回,后者根据是否大于span
选择插入到128 pages
或者n-k pages list
large span set
- 如果找不到合适的
,使用span
或者sbrk
向系统申请新的内存生成新的mmap
,再执行一次大对象的分配算法span
Golang内存管理
1.内存管理概览
的内存管理包含内存管理单元、线程缓存、中心缓存和页堆四个重要的组件,分别对应
Golang
、
runtine.mspan
、
runtime.mcache
和
runtime.mcentral
。
runtime.mheap
每一个
Go
程序在启动时都会向操作系统申请一块内存(仅仅是虚拟的地址空间,并不会真正分配内存),在
X64
上申请的内存会被分成
512M
、
16G
和
512G
的三块空间,分别对应
spans
、
bitmap
和
arena
。
-
:堆区,运行时该区域每arena
会被划分成一个页,存储了所有在堆上初始化的对象8KB
-
:标识bitmap
中哪些地址保存了对象,arena
中一个字节的内存对应bitmap
区域中arena
个指针大小的内存,并标记了是否包含指针和是否扫描的信息(一个指针大小为4
,因此8B
的大小为bitmap
)512GB/(4*8)=16GB
-
:存放spans
的指针,其中每个mspan
会包含多个页,mspan
中一个指针(spans
)表示8B
中某一个arena
(page
),因此8KB
的大小为spans
512GB/(1024)=512MB
2. 对象分级与多级缓存
golang
内存管理是在
tcmalloc
基础上实现的,同样实现了对象分级:
- 微对象:
- 小对象:
- 大对象:
类似于绝大多数对象是“朝生夕死”的,绝大对数对象的大小都小于 32KB
,对不同大小的对象分级管理并针对性实现对象的分配和回收算法有助于提升程序执行效率。
同样
golang
的内存管理也实现了
Thread Cache
、
Central Cache
和
PageHeap
的三级缓存,这样做的好处包括:
- 对于小对象的分配可以直接从
获取对应的内存空间,并且每个Thread Cache
是独立的因此无需加锁,极大提高了内存申请的效率Thread Cache
- 绝对多数对象都是小对象,因此这种做法可以保证大部分内存申请的操作是高效的
3. 内存管理组件
3.1 mspan
mspan
是由多个连续的
8KB
的页组成的内存空间,实现上是双端链表:
// go1.13.5
type mspan struct {
next *mspan // 链表下一个span地址
prev *mspan // 链表前一个span地址
list *mSpanList // 链表地址, 用于DEBUG
startAddr uintptr // 该span在arena区域的起始地址
npages uintptr // 该span占用arena区域page的数量
manualFreeList gclinkptr // 空闲对象列表
// freeindex是0~nelems的位置索引, 标记当前span中下一个空对象索引
freeindex uintptr
// 当前span中管理的对象数
nelems uintptr
allocCache uint64 // 从freeindex开始的位标记
allocBits *gcBits // 该mspan中对象的位图
gcmarkBits *gcBits // 该mspan中标记的位图,用于垃圾回收
sweepgen uint32
divMul uint16 // for divide by elemsize - divMagic.mul
baseMask uint16 // if non-0, elemsize is a power of 2, & this will get object allocation base
allocCount uint16 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
needzero uint8 // needs to be zeroed before allocation
divShift uint8 // for divide by elemsize - divMagic.shift
divShift2 uint8 // for divide by elemsize - divMagic.shift2
elemsize uintptr // computed from sizeclass or from npages
limit uintptr // end of data in span
speciallock mutex // guards specials list
specials *special // linked list of special records sorted by offset.
-
和startAddr
:确定该npages
管理的多个页在mspan
堆中的内存位置arena
-
和allocCache
:当用户程序或者线程向freeindex
申请内存时,根据这两个字段在管理的内存中快速查找可以分配的空间,如果查询不到可以分配的空间,mspan
会调用mcache
更新runtine.mcache.refill
以满足为更多对象分配内存的需求mspan
-
:决定spanclass
中存储对象mspan
的大小和个数,当前object
从golang
到8B
共分32KB
个类,下图是每一类大小能存储的66
大小和数量,以及因为内存按页管理造成的object
和最大内存浪费率。tail waster
假设一个的
mspan
为
spanclass
,那么它能存储的对象大小为
4
,能存储的对象个数为
33~48B
个,并且会在一页
170
的尾部浪费
8KB
的内存:
32B
假设
中存储的对象大小均为
mspan
,那么最大的内存浪费率
33B
为:
max waste
golang
在分配内存时会根据对象的大小来选择不同
spanclass
的
span
,比如
17~32Byte
的对象都会使用
spanclass
为
3
的
span
。超过
32KB
的对象被称为“大对象”,
golang
在分配这类“大对象”时会直接从
heap
中分配一个
spanclass
为
的
span
,它只包含一个大对象。
3.2 线程缓存
由于同一时间内只能有一个线程访问同一个
P
,因此
P
中的数据不需要加锁。每个
P
都绑定了
span
的缓存(被称为
mcache
)用于缓存用户程序申请的微对象,每一个
mcache
都拥有
67 * 2
个
mspan
。
scan和noscan的区别:
如果对象包含指针,分配对象时会使用scan的span;如果对象不包含指针,则分配对象时会使用noscan的span。如此在垃圾回收时对于noscan的span可以不用查看bitmap来标记子对象,提高GC效率。
type mcache struct {
alloc [numSpanClasses]*mspan
}
numSpanClasses = _NumSizeClasses << 1
mcache
在初始化时候是没有任何
mspan
资源的,在使用过程中动态地向
mcentrak
申请,之后会缓存下来。
3.3 中心缓存
mcentral
为所有线程的提供切分好的
mspan
资源,每个
mcentral
会保存一种特定大小的全局
mspan
列表,包括已分配出去的和未分配出去的。
type mcentral struct {
lock mutex // 互斥锁
sizeclass int32 // 规格
nonempty mSpanList // 尚有空闲object的mspan链表
empty mSpanList // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
nmalloc uint64 // 已累计分配的对象个数
}
mcache
和
mcentral
的交互:
-
没有足够的mcache
时加锁从mspan
的mcentral
中获取nonempty
并从mspan
删除,取出后加入nonempty
链表empty
-
归还时将mcache
从mspan
链表删除并重新添加回empty
noempty
3.4 mheap
golang
使用一个
mheap
的全局对象
_mheap
管理堆内存。主要功能包括:
- 在
无可用mecntral
时可以向mspan
申请,如果mheap
也没有资源的话就会向操作系统申请新内存mheap
- 用于大对象的分配
type mheap struct {
lock mutex
// spans: 指向mspans区域,用于映射mspan和page的关系
spans []*mspan
// 指向bitmap首地址,bitmap是从高地址向低地址增长的
bitmap uintptr
// 指示arena区首地址
arena_start uintptr
// 指示arena区已使用地址位置
arena_used uintptr
// 指示arena区末地址
arena_end uintptr
central [67*2]struct {
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
}
4. 分配流程
4.1 不同对象的分配流程
大对象:从
mheap
上申请内存
微对象:调用
mcache
的
tiny
分配器分配内存
小对象:根据占用空间向上取整由
mcache
中对应规格的
mspan
申请内存,如果没有合适的
mspan
则一级一级依次通过
mcentral
、
mheap
甚至操作系统申请内存空间
4.2 小对象的分配流程
- 首先从P的线程缓存mcache申请内存
- 然后从mcentral申请内存
- 最后从mheap申请内存
- 如果上述都获取失败,则从arena区域申请内存
golang从堆分配对象是通过runtime包中的nowobject函数实现的,函数执行流程如下:
Reference
[1] https://tonybai.com/2020/02/20/a-visual-guide-to-golang-memory-allocator-from-ground-up/
[2] https://juejin.im/post/5c888a79e51d456ed11955a8
[3] https://yq.aliyun.com/articles/652551
[4] https://www.cnblogs.com/vamei/p/9329278.html
[5] http://legendtkl.com/2015/12/11/go-memory/
[6] http://goog-perftools.sourceforge.net/doc/tcmalloc.html
[7] https://wallenwang.com/2018/11/tcmalloc/#ftoc-heading-17
[8] https://www.cnblogs.com/xumaojun/p/8547439.html
[9] https://zhuanlan.zhihu.com/p/73468738