爱上开源之golang入门至实战-第二章语言基础-内存管理

 

2.5.1.6 内存管理

作为入门学习一门编程语言;我们还是必须要对该语言编译及运行时的内存分配有一定的了解,这样可能你会更容易去深深的理解语言基础中的一些问题;这里我们也简要的了解一些Golang里变量的内存分配的基础知识

计算机组成里两个非常重要的概念:堆和栈

( Stack )是一种拥有特殊的线性表数据结构;栈只允许往线性表的的顶端放入数据,顶端取出数据,按照后进先出(LIFO,

Last In First Out )的顺序进行数据读写的操作;

 

往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数 ,最后放入的元素总是栈的顶部,最先放入的元素总是位于拢的底部。

从栈中取出元素时,只能从栈顶部取出,取出元素后,栈的元素数量会变少。最先放入栈的元素总是最后被取出,最后放入栈的元素总是最先被取出。 大家可以看到栈的这种数据存取的方式,和我们上面讲到的局部变量的作用域的方式很类似;的确,在很多语言里,对于局部变量的内存管理,都是使用的栈的这种数据结构进行管理;栈用于内存分配,分配和回收速度非常快;同样的,在Golang也是如此,Golang默认情况下会将局部变量分配在栈上,当局部变量的作用域结束(代码块结束);局部变量就不再使用,保存在栈的上内存进行出栈;并释放内存,整个分配内存和回收内存的过程通过战的分配和回收都会非常迅速;

( Heap )堆内存是区别于栈区、全局数据区和代码区的另一个内存区域。堆允许程序在运行时动态地申请某个大小的内存空间;Golang将堆地址空间划分成了一个一个的arena,在amd64架构的Linux环境下,每个arena的大小是64MB,起始地址也对齐到64MB,每个arena包含8192个page,所以每个page大小为8KB。但各空间分布可能不在连续的一段地址,而被分配在不同的区域,由于无法有一段连续的空间;所以堆内存空间可能在多次的分配回收过后;而使得空间出现混乱而碎片的情况,堆管理内存分配器就需要对这些空间进行调优,堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

堆与栈区别

堆:一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多

栈:由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上

内存分配策略

Golang的内存统一由内存管理器管理,Golang内存管理器是基于Google自身开源的TCMalloc内存分配器为理念设计和实现的;TCMalloc全称Thread Cache Memory alloc线程缓存内存分配器。顾名思义就是给线程添加内存缓存,减少竞争从而提高性能,当线程内存不足时才会加锁去共享的内存中获取内存。

Golang采用了和TCMalloc内存分配器一样的三层逻辑架构:

  • mcache:线程缓存

    Per-P(Processer,具体参见go中G,M,P的概念)私有cache,用于实现无锁的object分配

  • mcentral:中央缓存

    全局内存,为各个cache提供按大小划分好的span

  • mheap:堆内存

    全局内存,page管理,内存不足时向系统申请

中央缓存central是一个由136个mcentral类型元素的数组构成。

mcache被逻辑处理器p持有,而并不是被真正的系统线程m持有。

Golang通过将内存分配流程分为三个层级,既能保证Processer级别(mcache)的无锁分配,又能在mcentral级别实现内存全局共享,避免浪费。

 

Golang内存管理器将内存申请按大小分为三种类型:tiny,small,large。tiny是小于16个byte的申请,small是小于32KB的申请,大于32KB为large把申请的内存对象

三类内存对象如下:

  • 微对象 0 < Micro Object < 16B

  • 小对象 16B =< Small Object <= 32KB

  • 大对象 32KB < Large Object

为了清晰看出这三层的关系,这里以堆上分配小对象为例:

  • 先去线程缓存mcache中分配内存

  • 找不到时,再去中央缓存central中分配内存

  • 最后直接去堆上mheap分配一块内存 对应SizeClass的PageHeap中分配

  • large对象的申请,跳过了mcache和mcentral

内存申请流程:

  1. 计算对象大小,按预定义的sizeclass表(见下)从私有的mcache中找到对应规格的mspan。比如大小为112 byte的对象,对应8192 byte大小的mspan。然后通过mspan的空闲bitmap查找空闲的块,如果空闲块存在,分配完成。

    以上是mcache内的分配操作,不需要加锁。

  2. 如果mspan没有空闲块,则向mcentral申请对应大小的空闲mspan。比如112 byte的对象,需要向mcentral申请8192 byte大小的空闲mspan。

    由于申请获取全局的mspan,需要在mcentral级别加锁。

  3. 如果mcentral中没有空闲mspan,则向mheap申请,并划分object。

  4. 如果mheap没有足够的空闲page,则向操作系统申请不少于1M的page。

sizeclass表

// sizeclass
// class  bytes/obj  bytes/span  objects  waste bytes
//     1          8        8192     1024            0
//     2         16        8192      512            0
//     3         32        8192      256            0
//     4         48        8192      170           32
//     5         64        8192      128            0
//     6         80        8192      102           32
//     7         96        8192       85           32
//     8        112        8192       73           16
//     9        128        8192       64            0
//    10        144        8192       56          128
//    11        160        8192       51           32
//    12        176        8192       46           96
//    13        192        8192       42          128
//    14        208        8192       39           80
//    15        224        8192       36          128
//    16        240        8192       34           32
//    17        256        8192       32            0
//    18        288        8192       28          128
//    19        320        8192       25          192
//    20        352        8192       23           96
//    21        384        8192       21          128
//    22        416        8192       19          288
//    23        448        8192       18          128
//    24        480        8192       17           32
//    25        512        8192       16            0
//    26        576        8192       14          128
//    27        640        8192       12          512
//    28        704        8192       11          448
//    29        768        8192       10          512
//    30        896        8192        9          128
//    31       1024        8192        8            0
//    32       1152        8192        7          128
//    33       1280        8192        6          512
//    34       1408       16384       11          896
//    35       1536        8192        5          512
//    36       1792       16384        9          256
//    37       2048        8192        4            0
//    38       2304       16384        7          256
//    39       2688        8192        3          128
//    40       3072       24576        8            0
//    41       3200       16384        5          384
//    42       3456       24576        7          384
//    43       4096        8192        2            0
//    44       4864       24576        5          256
//    45       5376       16384        3          256
//    46       6144       24576        4            0
//    47       6528       32768        5          128
//    48       6784       40960        6          256
//    49       6912       49152        7          768
//    50       8192        8192        1            0
//    51       9472       57344        6          512
//    52       9728       49152        5          512
//    53      10240       40960        4            0
//    54      10880       32768        3          128
//    55      12288       24576        2            0
//    56      13568       40960        3          256
//    57      14336       57344        4            0
//    58      16384       16384        1            0
//    59      18432       73728        4            0
//    60      19072       57344        3          128
//    61      20480       40960        2            0
//    62      21760       65536        3          256
//    63      24576       24576        1            0
//    64      27264       81920        3          128
//    65      28672       57344        2            0
//    66      32768       32768        1            0
​

分配过程,简而言之如下图所示

 

最后汇总内存管理大致策略:(go1.18)

  • 申请一块较大的地址空间(虚拟内存),用于内存分配及管理(golang:spans+bitmap+arena->512M+16G+512G)

  • 当空间不足时,向系统申请一块较大的内存,如100KB或者1MB

  • 申请到的内存块按特定的size,被分割成多种小块内存(如上sizeclass),并用链表管理起来

  • 创建对象时,按照对象大小,从空闲链表中查找到最适合的内存块

  • 销毁对象时,将对应的内存块返还空闲链表中以复用

  • 空闲内存达到阈值时,返还操作系统

在Golang语⾔⾥,从内存的分配到不再使⽤后内存的回收等等这些内存管理⼯作都是由在底层完成的。虽然开发者在写代码时不必过度⼼内存从分配到回收这个过程,但是的内存分配策略⾥有不少有意思的设计,通过了解他们有助于我们⾃⾝的提⾼,也让我们能写出更⾼ 效的Golang的代码。


版权声明:本文为inthirties原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。