天天看点

0x00000000指令引用的内存不能为written_Golang内存管理

物理内存与虚拟内存

0. CPU与内存

内存的最小存储单位为一个字节,我们通过一个十六进制的数据表示每个字节的编号(比如

4G

内存的十六进制表示从

0x0000 0000

0xffff ffff

)。其中内存地址的编号上限由地址总线

address bus

的位数相关,

CPU

通过地址总线来表示向内存说明想要存储数据的地址。

32

CPU

为例,

CPU

包含

32

个针脚来传递地址信息,每个针脚能传递的信息为

1bit

(即

0 | 1

),对应的地址空间大小为

2^32 = 4G

0x00000000指令引用的内存不能为written_Golang内存管理

同其他存储介质相比,内存的存储单元采用了随机读取存储器

RAM, Random Access Memory

,存储器读取数据花费的时间和数据所在的位置无关(而磁盘和磁带只能顺序访问数据,有损于速度和效率)。这个特性使得系统可以把控进程的运行时间,这也是内存称为主存储器的关键因素。内存的缺点是不能持久化数据,计算机一旦断电内存中的数据就会消失。

1. 早期的内存分配方式

在一开始是没有虚拟内存的概念的,程序访问的地址就是真实的物理地址,

CPU

可以直接根据寄存器中的值去访问对应的物理内存。计算机按照每个程序需要的内存大小进行分配,这种做法虽然看上去比较直观,但是存在如下几个问题:

  1. 程序员需要额外关心内存布局:假设内存总大小为

    n

    ,第一个程序使用了

    m

    数量的内存空间后,第二个程序使用的内存范围即为

    m~n

    ,需要妥善处理内存偏移
  2. 内存使用效率低下:假如系统没有足够的空间运行新的程序时,需要选择一个已运行的程序拷贝到硬盘上释放出内存空间供新的程序使用,这种多余的

    IO

    操作会降低计算机的运行效率
  3. 安全风险较高:不同进程的地址空间不隔离,一方面难以阻止恶意程序修改别的进程内存数据,另一方面某个程序的

    bug

    也可能会污染其他进程的内存数据
0x00000000指令引用的内存不能为written_Golang内存管理

2. 虚拟内存

虚拟内存在程序访问真实的物理地址之间封装了一层,每个进程的内存读写都是建立在属于该进程的虚拟内存上的,而虚拟内存映射到物理内存的工作统一交给操作系统处理。程序员既不需要担心不同进程的内存地址冲突的问题,又不需要关心硬件层面上虚拟内存是如何映射到物理内存的。

0x00000000指令引用的内存不能为written_Golang内存管理

这种映射带来的好处包括:

  • 以程序员的视角来看,每个程序申请的虚拟内存都是从 开始的连续空间,既不需要再去手动维护内存地址的偏移,也不需要担心不同程序之间的内存地址冲突
  • 突破了物理内存大小的限制:操作系统分配给每个进程的虚拟进程可以比实际的物理内存大得多
  • 虚拟内存中的连续空间不需要映射到物理内存中的连续空间,可以有效利用内存碎片

3. 进程与虚拟地址的关系

Linux

进程:程序编译后的可执行文件是放在磁盘上的,执行时首先将进程加载到内存中然后再放到寄存器中,最后让

CPU

执行程序,一个静态的程序就变成了进程。

进程在运行的时候相关数据需要存储在内存中,但是出于数据隔离性和安全性的考虑进程不能通过

0x 0000 0001

的方式直接访问物理内存,只能通过虚拟地址访问。以

Linux

为例,每个进程都有属于自己的

4GB

虚拟内存空间,虚拟地址到物理地址的翻译和不同进程的物理地址隔离是交给操作系统来实现的。

虚拟地址的实现禁止了进程空间访问物理内存地址,统一交给操作系统进行管理,这带来几个好处:

  • 对于每个线程独享的数据,操作系统只需要将每个进程对应的虚拟空间地址映射到不同的物理空间地址就可以保证两个进程的执行互不干扰
  • 对于需要”内存共享“的数据实现起来也很简单:只需要把多个进程的虚拟内存空间对应到同一物理内存空间即可(比如内存和共享库的实现)。
0x00000000指令引用的内存不能为written_Golang内存管理

4. 分页

分页的核心思想是可执行文件的虚拟进程空间分成大小相同的多页,没执行到的页暂时保留在硬盘上,执行到哪一页就为该页在物理地址空间中分配内存,同时建立虚拟地址和物理地址页的映射关系。

尽管在真实的物理地址上封装了一层虚拟地址提高了程序设计的效率和安全性,但是操作系统需要额外耗费资源把进程的虚拟地址翻译成物理地址。在虚拟地址映射到物理地址的过程中,有几个问题需要考虑到:

  • 为了高效地“翻译”虚拟地址,这个映射关系必须加载在内存中
  • 程序在运行的时候具有局部性特点:在程序运行的某个具体的时间点只有小部分的数据会被访问到
  • 如果虚拟地址中每个字节都维护一个对应记录的话,那么光是对应关系就已经耗费完内存的空间了

基于上述事实,

Linux

引入“分页”

paging

的方式来记录对应关系,即使用更大尺寸(一般为

4KB

)的单位

page

来管理内存,

Linux

中物理内存和进程虚拟内存都被分割成页。

paging

具有如下特点:

  • 一页之内的地址是连续的:这样虚拟页和物理页一旦联系到一起,就意味着页内的顺序可以按照顺序一一对应起来
  • 可以极大减少虚拟内存和物理内存的对应关系,如果页的大小是

    4KB

    ,那么需要维护的对应关系就减少为​
  • 由于

    4096

    2

    12

    次方,因此地址最后

    12

    位可以同时表示虚拟地址和物理地址的偏移量

    offset

    ,即该字节在页内的位置,地址的前一部分表示页编号,操作系统就只需要维护虚拟地址和物理地址的页编号对应关系
图源: https://www. cnblogs.com/vamei/p/932 9278.html
0x00000000指令引用的内存不能为written_Golang内存管理

当进程需要读写内存时,首先唤醒

MMU

根据虚拟空间地址的页编号映射到真实的物理页,再根据偏移量找到实际的物理地址进行读写。如果程序尝试去访问一个没有映射到物理页的虚拟地址时(这种情况被称为缺页中断),操作系统会建立虚拟地址和物理地址的映射关系到页表中,如果物理内存不够用时操作系统会覆盖掉某个页(被覆盖的页如果曾经被修改过,那么需要将此页写回磁盘)。

Q:从这个角度说,在运行程序的时候,磁盘上可能存储着除可执行文件本身外的数据内容,即

4G

虚拟内存的一部分?

虚拟内存分布

我们编写的程序都运行在计算机的内存(一般指的是计算机的随机存储器

RAM

),

UNIX

环境内存的大致分布如下:

当应用程序被加载到内存空间中执行时,操作系统负责代码段、数据段和

BSS

段的加载,并在内存中为这些段分配空间。在程序运行期间,栈段也交给操作系统进行管理,只有堆段是程序员自行管理(可显式地申请和释放空间)的内存区域。
0x00000000指令引用的内存不能为written_Golang内存管理

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. 线性分配

线性分配算法是最简单的内存分配算法,通过一个指针指向空闲内存的首地址。当用户程序申请内存时,根据用户申请内存的大小返回指针后面相应大小的区域,并更新指针。这种方法有两个卓越的优点:

  • 实现成本低
  • 分配速度快
0x00000000指令引用的内存不能为written_Golang内存管理

但这种简单粗暴的算法也有显著的缺点,比如当已分配内存的对象

2

成为垃圾对象时,我们无法回收对象

2

的内存用于下一个对象的分配。

0x00000000指令引用的内存不能为written_Golang内存管理

2. 空闲链表分配

空闲链表

Free-List

会用一个链表存储所有的空闲内存,当用户程序申请内存时,扫描空闲内存链表并取出大小满足要求的内存块。已分配内存的对象成为垃圾对象时,对应的内存块会重新插入到空闲链表中。

0x00000000指令引用的内存不能为written_Golang内存管理

一般而言空闲链表中有很多的内存块都可以满足用户程序申请内存块的大小要求,依据选取策略的不同我们将空闲链表分配划分为如下几种:

  • 首先匹配

    First-Fit

    :从空闲链表头部开始扫描,返回第一个满足大小的空闲内存块
  • 最佳匹配

    Best-Fit

    :遍历空闲链表,返回满足大小的最小空闲内存块
  • 最差匹配

    Worst-Fit

    :遍历空闲链表,如果找不到正好符合大小的空闲内存块,从最大的空闲内存块中分配
  • 隔离匹配

    Segregated-Fit

    :将空闲内存分为多个存储固定大小的链表,首先根据用户程序申请的内存大小确定合适的链表,再从链表中获取空闲内存块

隔离匹配综合了其他匹配方法的优势,减少了对空闲内存块的遍历,

Golang

的内存分配策略也是基于该匹配方法实现的。隔离匹配会将内存分割成

4B

8B

16B

32B

等多个链表,比如当我们向内存分配器申请

8B

的内存时,内存分配器会从第二个链表中找到空闲的内存块。

0x00000000指令引用的内存不能为written_Golang内存管理

Golang内存管理前身:tcmalloc

1. 简介

tcmalloc

全称为

thread-caching malloc

,是

google

推出的一种内存管理器。按照对象所占内存空间的大小,

tcmalloc

将对象划分为三类:

  • 小对象:​
    0x00000000指令引用的内存不能为written_Golang内存管理
  • 中对象:​
    0x00000000指令引用的内存不能为written_Golang内存管理
  • 大对象:​
    0x00000000指令引用的内存不能为written_Golang内存管理

相关的重要概念:

  • 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

0x00000000指令引用的内存不能为written_Golang内存管理

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

0x00000000指令引用的内存不能为written_Golang内存管理
  • arena

    :堆区,运行时该区域每

    8KB

    会被划分成一个页,存储了所有在堆上初始化的对象
  • bitmap

    :标识

    arena

    中哪些地址保存了对象,

    bitmap

    中一个字节的内存对应

    arena

    区域中

    4

    个指针大小的内存,并标记了是否包含指针和是否扫描的信息(一个指针大小为

    8B

    ,因此

    bitmap

    的大小为

    512GB/(4*8)=16GB

0x00000000指令引用的内存不能为written_Golang内存管理
0x00000000指令引用的内存不能为written_Golang内存管理
  • spans

    :存放

    mspan

    的指针,其中每个

    mspan

    会包含多个页,

    spans

    中一个指针(

    8B

    )表示

    arena

    中某一个

    page

    8KB

    ),因此

    spans

    的大小为

    512GB/(1024)=512MB

0x00000000指令引用的内存不能为written_Golang内存管理

2. 对象分级与多级缓存

golang

内存管理是在

tcmalloc

基础上实现的,同样实现了对象分级:

  • 微对象:​
    0x00000000指令引用的内存不能为written_Golang内存管理
  • 小对象:​
    0x00000000指令引用的内存不能为written_Golang内存管理
  • 大对象:​
    0x00000000指令引用的内存不能为written_Golang内存管理
类似于绝大多数对象是“朝生夕死”的,绝大对数对象的大小都小于

32KB

,对不同大小的对象分级管理并针对性实现对象的分配和回收算法有助于提升程序执行效率。

同样

golang

的内存管理也实现了

Thread Cache

Central Cache

PageHeap

的三级缓存,这样做的好处包括:

  • 对于小对象的分配可以直接从

    Thread Cache

    获取对应的内存空间,并且每个

    Thread Cache

    是独立的因此无需加锁,极大提高了内存申请的效率
  • 绝对多数对象都是小对象,因此这种做法可以保证大部分内存申请的操作是高效的

3. 内存管理组件

3.1 mspan

0x00000000指令引用的内存不能为written_Golang内存管理

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

的内存:
0x00000000指令引用的内存不能为written_Golang内存管理

假设

mspan

中存储的对象大小均为

33B

,那么最大的内存浪费率

max waste

为:
0x00000000指令引用的内存不能为written_Golang内存管理
0x00000000指令引用的内存不能为written_Golang内存管理

golang

在分配内存时会根据对象的大小来选择不同

spanclass

span

,比如

17~32Byte

的对象都会使用

spanclass

3

span

。超过

32KB

的对象被称为“大对象”,

golang

在分配这类“大对象”时会直接从

heap

中分配一个

spanclass

span

,它只包含一个大对象。

3.2 线程缓存

由于同一时间内只能有一个线程访问同一个

P

,因此

P

中的数据不需要加锁。每个

P

都绑定了

span

的缓存(被称为

mcache

)用于缓存用户程序申请的微对象,每一个

mcache

都拥有

67 * 2

mspan

0x00000000指令引用的内存不能为written_Golang内存管理

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区域申请内存
0x00000000指令引用的内存不能为written_Golang内存管理

golang从堆分配对象是通过runtime包中的nowobject函数实现的,函数执行流程如下:

0x00000000指令引用的内存不能为written_Golang内存管理

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

继续阅读