美文网首页
Glang 内存管理

Glang 内存管理

作者: 小道萧兮 | 来源:发表于2023-08-11 11:49 被阅读0次

    1、虚拟内存

    虚拟内存是当代操作系统必备的一项重要功能,对于进程而言虚拟内存屏蔽了底层了RAM和磁盘,并向进程提供了远超物理内存大小的内存空间。我们看一下虚拟内存的分层设计。

    上图展示了某进程访问数据,访问虚拟内存获取数据的过程。在访问内存,实际访问的是虚拟内存,虚拟内存通过页表查看,当前要访问的虚拟内存地址,是否已经加载到了物理内存。如果已经在物理内存,则取物理内存数据,如果没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。

    物理内存就是磁盘存储缓存层,在没有虚拟内存的时代,物理内存对所有进程是共享的,多进程同时访问同一个物理内存会存在并发问题。而引入虚拟内存后,每个进程都有各自的虚拟内存,内存的并发访问问题的粒度从多进程级别,可以降低到多线程级别。

    堆内存管理

    当我们说内存管理的时候,主要是指堆内存的管理,因为栈的内存管理不需要程序去操心,

    如上图所示主要是3部分,分别是分配内存块,回收内存块和组织内存块。

    在一个最简单的内存管理中,堆内存最初会是一个完整的大块,即未分配任何内存。当发现内存申请的时候,堆内存就会从未分配内存分割出一个小内存块(block),然后用链表把所有内存块连接起来。需要一些信息描述每个内存块的基本信息,比如大小(size)、是否使用中(used)和下一个内存块的地址(next),内存块实际数据存储在data中。

    一个内存块包含了3类信息,如下图所示,元数据、用户数据和对齐字段,内存对齐是为了提高访问效率。下图申请5Byte内存的时候,就需要进行内存对齐。

    对于一个结构体来说,占用内存大小就应该等于多个基础类型占用内存大小的和,我们就结合几个示例来看下:

    type Example struct {
        a bool // 1个字节
        b int    // 8个字节
        c string // 16个字节
    }
    
    func main() {
        fmt.Println(unsafe.Sizeof(Example{})) // 32
    }
    

    Example 结构体的三个基础类型,加起来一个 25字节,但是最终输出的却是 32字节。

    我们再看两个结构体,即使这两个结构体包含的字段类型一致,但是顺序不一致,最终输出的大小也不一样:

    type A struct {
        a int32
        b int64
        c int32
    }
    
    type B struct {
        a int32
        b int32
        c int64
    }
    
    func main() {
        fmt.Println(unsafe.Sizeof(A{})) // 24
        fmt.Println(unsafe.Sizeof(B{})) // 16
    }
    

    是什么导致了上述问题的呢,这就引出了我们要看的知识点:内存对齐。

    CPU 访问内存时并不是逐个字节访问,而是以字长为单位访问,例如 32 位的CPU 字长是 4 字节,64 位的是 8 字节。如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量。

    [图片上传失败...(image-154c59-1691812172519)]

    释放内存实质是把使用的内存块从链表中取出来,然后标记为未使用,当分配内存块的时候,可以从未使用内存块中优先查找大小相近的内存块,如果找不到,再从未分配的内存中分配内存。

    TCMalloc

    TCMalloc是 Thread Cache Malloc 的简称,是Go内存管理的起源,Go的内存管理是借鉴了TCMalloc,随着Go的迭代,Go的内存管理与TCMalloc不一致地方在不断扩大,但其主要思想、原理和概念都是和TCMalloc一致的

    前面提到引入虚拟内存后,让内存的并发访问问题的粒度从多进程级别,降低到多线程级别。然而同一进程下的所有线程共享相同的内存空间,它们申请内存时需要加锁,如果不加锁就存在同一块内存被2个线程同时访问的问题。

    TCMalloc的做法是什么呢?为每个线程预分配一块缓存,线程申请小内存时,可以从缓存分配内存,这样有2个好处:

    1. 为线程预分配缓存需要进行1次系统调用,后续线程申请小内存时直接从缓存分配,都是在用户态执行的,没有了系统调用,缩短了内存总体的分配和释放时间,这是快速分配内存的第二个层次。
    2. 多个线程同时申请小内存时,从各自的缓存分配,访问的是不同的地址空间,从而无需加锁,把内存并发访问的粒度进一步降低了,这是快速分配内存的第三个层次。

    下面就简单介绍下TCMalloc

    Page
    操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。

    Span
    一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。

    ThreadCache
    ThreadCache是每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。

    CentralCache
    CentralCache是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache的内存块不足时,可以从CentralCache获取内存块;当ThreadCache内存块过多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。

    PageHeap
    PageHeap是对堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span。当CentralCache的内存不足时,会从PageHeap获取空闲的内存Span,然后把1个Span拆成若干内存块,添加到对应大小的链表中并分配内存;当CentralCache的内存过多时,会把空闲的内存块放回PageHeap中。

    如下图所示,分别是1页Page的Span链表,2页Page的Span链表等,最后是large span set,这个是用来保存中大对象的。毫无疑问,PageHeap也是要加锁的。

    前文提到了小、中、大对象,Go内存管理中也有类似的概念,我们看一眼TCMalloc的定义:

    小对象大小:0~256KB
    中对象大小:257~1MB
    大对象大小:>1MB
    小对象的分配流程:ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache缓存都是足够的,不需要去访问CentralCache和HeapPage,无系统调用配合无锁分配,分配效率是非常高的。

    中对象分配流程:直接在PageHeap中选择适当的大小即可,128 Page的Span所保存的最大内存就是1MB。

    大对象分配流程:从large span set选择合适数量的页面组成span,用来存储数据。

    Go内存管理的基本概念

    Go内存管理的许多概念在TCMalloc中已经有了,含义是相同的,只是名字有一些变化。

    内存单元 mspan

    mspan 是 Golang 内存管理的最小单元
    mspan 大小是 page 的整数倍(Go 中的 page 大小为 8KB),且内部的页是连续的(在虚拟内存的视角中是这样)
    每个 mspan 根据空间大小以及面向分配对象的大小,会被划分为不同的等级
    npages:表示当前span包含多少个页,npages是根据spanclass来确定的。一个 page 是 8kb,也就是这个 span 是 npages * 8k 大小内存。
    同等级的 mspan 会从属同一个 mcentral,最终会被组织成链表,因此带有前后指针(prev、next)
    由于同等级的 mspan 内聚于同一个 mcentral,所以会基于同一把互斥锁管理
    spanclass:用于计算当前 span 分配对象的大小。spanClass 的值为0-66,每一个值分别对应一个分配对象的大小以及页数。比如spanclass 为 1,则 span 用于分配 8byte 的对象,且当前 span 占用一个 page 的存储,也就是 span 大小是 8kb。

    mspan 类的源码位于 runtime/mheap.go 文件中:

    type mspan struct {
        // 标识前后节点的指针 
        next *mspan     
        prev *mspan    
        // ...
        // 起始地址
        startAddr uintptr 
        // 包含几页,页是连续的
        npages    uintptr 
    
    
        // 标识此前的位置都已被占用 
        freeindex uintptr
        // 最多可以存放多少个 object
        nelems uintptr // number of object in the span.
    
    
        // bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
        allocCache uint64
        // ...
        // 标识 mspan 等级,包含 class 和 noscan 两部分信息
        spanclass             spanClass    
        // ...
    }
    
    //  sizeclasses.go
    // class  bytes/obj  bytes/span  objects  tail waste  max waste
    //     1          8        8192     1024           0     87.50%
    //     2         16        8192      512           0     43.75%
    //     3         32        8192      256           0     46.88%
    //     4         48        8192      170          32     31.52%
    //     5         64        8192      128           0     23.44%
    //     6         80        8192      102          32     19.07%
    //     7         96        8192       85          32     15.95%
    //     8        112        8192       73          16     13.56%
    //     9        128        8192       64           0     11.72%
    //    10        144        8192       56         128     11.82%
    //    11        160        8192       51          32      9.73%
    //    12        176        8192       46          96      9.59%
    //    13        192        8192       42         128      9.25%
    //    14        208        8192       39          80      8.12%
    //    15        224        8192       36         128      8.15%
    //    16        240        8192       34          32      6.62%
    //    17        256        8192       32           0      5.86%
    //    18        288        8192       28         128     12.16%
    //    19        320        8192       25         192     11.80%
    //    20        352        8192       23          96      9.88%
    //    21        384        8192       21         128      9.51%
    //    22        416        8192       19         288     10.71%
    //    23        448        8192       18         128      8.37%
    //    24        480        8192       17          32      6.82%
    //    25        512        8192       16           0      6.05%
    //    26        576        8192       14         128     12.33%
    //    27        640        8192       12         512     15.48%
    //    28        704        8192       11         448     13.93%
    //    29        768        8192       10         512     13.94%
    //    30        896        8192        9         128     15.52%
    //    31       1024        8192        8           0     12.40%
    //    32       1152        8192        7         128     12.41%
    //    33       1280        8192        6         512     15.55%
    //    34       1408       16384       11         896     14.00%
    //    35       1536        8192        5         512     14.00%
    //    36       1792       16384        9         256     15.57%
    //    37       2048        8192        4           0     12.45%
    //    38       2304       16384        7         256     12.46%
    //    39       2688        8192        3         128     15.59%
    //    40       3072       24576        8           0     12.47%
    //    41       3200       16384        5         384      6.22%
    //    42       3456       24576        7         384      8.83%
    //    43       4096        8192        2           0     15.60%
    //    44       4864       24576        5         256     16.65%
    //    45       5376       16384        3         256     10.92%
    //    46       6144       24576        4           0     12.48%
    //    47       6528       32768        5         128      6.23%
    //    48       6784       40960        6         256      4.36%
    //    49       6912       49152        7         768      3.37%
    //    50       8192        8192        1           0     15.61%
    //    51       9472       57344        6         512     14.28%
    //    52       9728       49152        5         512      3.64%
    //    53      10240       40960        4           0      4.99%
    //    54      10880       32768        3         128      6.24%
    //    55      12288       24576        2           0     11.45%
    //    56      13568       40960        3         256      9.99%
    //    57      14336       57344        4           0      5.35%
    //    58      16384       16384        1           0     12.49%
    //    59      18432       73728        4           0     11.11%
    //    60      19072       57344        3         128      3.57%
    //    61      20480       40960        2           0      6.87%
    //    62      21760       65536        3         256      6.25%
    //    63      24576       24576        1           0     11.45%
    //    64      27264       81920        3         128     10.00%
    //    65      28672       57344        2           0      4.91%
    //    66      32768       32768        1           0     12.50%
    
    const (
        _MaxSmallSize   = 32768
        smallSizeDiv    = 8
        smallSizeMax    = 1024
        largeSizeDiv    = 128
        _NumSizeClasses = 67
        _PageShift      = 13
    )
    
    var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
    var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
    
    
    

    线程缓存 mcache

    mcache与TCMalloc中的ThreadCache类似,mcache保存的是各种大小的Span,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问。但是mcache与ThreadCache也有不同点,TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcache。

    中心缓存 mcentral

    mcentral与TCMalloc中的CentralCache类似,是所有线程共享的缓存,需要加锁访问。它按Span级别对Span分类,然后串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。

    // Central list of free objects of a given size.
    //go:notinheap
    type mcentral struct {
        lock      mutex
        spanclass spanClass
        nonempty  mSpanList // list of spans with a free object, ie a nonempty free list
        empty     mSpanList // list of spans with no free objects (or cached in an mcache)
    
        // nmalloc is the cumulative count of objects allocated from
        // this mcentral, assuming all spans in mcaches are
        // fully-allocated. Written atomically, read under STW.
        nmalloc uint64
    }
    

    全局堆缓存 mheap

    mheap与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS申请出的内存页组织成Span,并保存起来。当mcentral的Span不够用时会向mheap申请内存,而mheap的Span不够用时会向OS申请内存。mheap向OS的内存申请是按页来的,然后把申请来的内存页生成Span组织起来,同样也是需要加锁访问的。

    相关文章

      网友评论

          本文标题:Glang 内存管理

          本文链接:https://www.haomeiwen.com/subject/fmnxpdtx.html