golang学习笔记(内存模型和分配机制)

news2024/11/19 2:20:43

操作系统的存储管理

虚拟内存管理

虚拟内存是一种内存管理技术,它允许操作系统为每个进程提供一个比实际物理内存更大的地址空间。这个地址空间被称为虚拟地址空间,而实际的物理内存则被称为物理地址空间。使用虚拟内存有以下几点好处:

内存抽象:虚拟内存为每个进程提供了一个连续的地址空间,使得每个程序都认为自己独占了整个内存,这样可以简化程序的编写和调试,开发人员不需要关心物理内存的具体布局。

内存保护:通过虚拟内存,每个进程都有自己的地址空间,互不干扰。这样可以防止一个程序错误地访问或修改另一个程序的内存,提高了系统的稳定性和安全性。

内存共享:虚拟内存允许多个进程共享相同的物理内存页,例如,多个进程可以共享相同的库或程序代码,这样可以节省内存空间。

支持更多的应用程序:虚拟内存通过使用硬盘空间作为额外的内存,在物理内存不足时,可以临时将不常用的内存页移出到硬盘上,从而释放内存空间。这样,系统就可以运行更多的应用程序,即使物理内存有限。

更有效的内存使用:操作系统可以通过虚拟内存管理机制,如页面置换算法,来优化内存的使用效率,确保最常用的数据和程序保留在物理内存中,而较少使用的部分则可以暂时存储在硬盘上。
在这里插入图片描述

进程的虚拟内存管理

每个进程的虚拟内存空间都会映射到物理内存,如何映射对于程序员来说是透明的不需要关心,虚拟内存到物理内存的映射由操作系统实现。内核态虚拟内存空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。比如下图中进程 1、进程 2 在内核态都去访问虚拟地址,由于内核态这部分地址空间映射到的都是同一份物理内存,进程 1、进程 2 看到的内容也是一样的。
在这里插入图片描述
进程的用户空间被分为多个段:

  • 代码段:在进程运行之前,存放在二进制文件中的机器码需要被加载进内存中,用于存放这些机器码的虚拟内存空间叫做代码段。

  • 数据段:指定初始值的全局变量和静态变量,在程序运行之前这些全局变量和静态变量会被加载进内存中供程序访问,这段区域叫做数据段。

  • BBS段:没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域叫做 BSS 段,这些未初始化的全局变量被加载进内存之后会被初始化为 0 值。BSS 段与数据段的区别是不会一开始就加入到内存中而是需要的时候从硬盘上加载。

  • :程序在运行期间往往需要动态的申请内存,所以在虚拟内存空间中也需要一块区域来存放这些动态申请的内存,这块区域就叫做堆。

  • :在程序运行的时候会调用函数,调用函数过程中使用到的局部变量和函数参数也需要一块内存区域来保存。这一块区域在虚拟内存空间中叫做栈。

  • 文件映射与匿名映射区:每当我们调用一次 mmap 进行内存映射的时候,内核都会在文件映射与匿名映射区中划分出一段虚拟映射区出来,这段虚拟映射区就是我们申请到的虚拟内存。

分页管理

操作系统中通常会将虚拟内存和物理内存切割成固定的尺寸,于虚拟内存而言叫作“页”,于物理内存而言叫作“帧”,原因及要点如下:

  • 提高内存空间利用(以页为粒度后,消灭了不稳定的外部碎片,取而代之的是相对可控的内部碎片)
  • 提高内外存交换效率(更细的粒度带来了更高的灵活度)
  • 与虚拟内存机制呼应,便于建立虚拟地址->物理地址的映射关系(聚合映射关系的数据结构,称为页表)
  • linux 页/帧的大小固定,为 4KB(这实际是由实践推动的经验值,太粗会增加碎片率,太细会增加分配频率影响效率)

golang的内存管理

基础概念

为了方便自主管理内存, 做法便是先向系统申请一块内存, 然后将内存切割成小块, 通过一定的内存分配算法管理内存。 以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示:
在这里插入图片描述
预申请的内存划分为spans、 bitmap、 arena三部分。 其中arena即为所谓的堆区, 应用中需要的内存从这里分配。其中spans和bitmap是为了管理arena区而存在的。

arena的大小为512G, 为了方便管理把arena区域划分成一个个的page, 每个page为8KB,一共有512GB/8KB个页;

spans区域存放span的指针, 每个指针对应一个page, 所以span区域的大小为(512GB/8KB)*指针大小8byte =512M

bitmap区域大小也是通过arena计算出来, 不过主要用于GC。

内存池概念

在程序中变量的存储位置通常分布在全局数据区、栈区和堆区这三个内存区域:

  • 堆区: 主要承担运行时的动态内存分配任务,它为程序提供灵活的内存使用空间。
  • 全局数据区: 专门用于存储全局变量,这些变量在程序的整个生命周期内都存在。
  • 栈区: 以栈帧为基本单位进行分配,它负责存储函数的参数、返回值以及局部变量。

golang的内存架构

golang的内存分配主要有三个组件
mcache:Per-P(Processer,具体参见go中G,M,P的概念)私有cache,用于实现无锁的object分配。
mcentral:全局内存,为各个cache提供按大小划分好的span,每种对象大小规格(全局共划分为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内
mheap:全局内存,page管理,内存不足时向系统申请,访问要加全局锁

page和mspan的概念

在此梳理一下span和mspan的概念:

  1. page:最小的存储单元,Golang 借鉴操作系统分页管理的思想,每个最小的存储单元也称之为页 page,但大小为 8 KB。

  2. mspan: 大小为 page 的整数倍,且从 8B 到 80 KB 被划分为 67 种不同的规格,分配对象时,会根据大小映射到不同规格的 mspan,从中获取空间。

在这里插入图片描述

mspan 的数据结构

span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小, span将一个或多个页拆分成多个块进行管理。源码位于 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.
	allocBits *gcBits //分配位图, 每一位代表一个块是否已分配
	allocCount uint16 // 已分配块的个数
    // bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
    allocCache uint64
    // ...
    // 标识 mspan 等级,包含 class 和 noscan 两部分信息
    spanclass             spanClass   
    elemsize uintptr // class表中的对象大小, 也即块大小
    // ...
}

以class 10为例, span和管理的内存如下图所示:
在这里插入图片描述
spanclass为10, 参照class表(下文有)可得出npages=1,nelems=56,elemsize为144。 其中startAddr是在span初始化时就指定了某个页的地址。 allocBits指向一个位图, 每位代表一个块是否被分配, 本例中有两个块已经被分配,其allocCount也为2。next和prev用于将多个span链接起来, 这有利于管理多个span。

mspan 根据空间大小和面向分配对象的大小,被划分为 67 种等级(1-67,实际上还有一种隐藏的 0 级,用于处理更大的对象,上不封顶

// 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

在 Golang 中,会将 spanClass + nocan 两部分信息组装成一个 uint8,形成完整的 spanClass 标识. 8 个 bit 中,高 7 位表示了上表的 span 等级(总共 67 + 1 个等级,8 个 bit 足够用了),最低位表示 nocan 信息。代码位于 runtime/mheap.go

type spanClass uint8

const (
	numSpanClasses = _NumSizeClasses << 1
	tinySpanClass  = spanClass(tinySizeClass<<1 | 1)
)
// uint8 左 7 位为 mspan 等级,最右一位标识是否为 noscan
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
	return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

func (sc spanClass) sizeclass() int8 {
	return int8(sc >> 1)
}

func (sc spanClass) noscan() bool {
	return sc&1 != 0
}

线程缓存 mcache

管理内存的基本单位span, 还要有个数据结构来管理span, 这个数据结构叫mcentral, 各线程需要内存时从mcentral管理的span中申请内存, 为了避免多线程申请内存时不断的加锁, Golang为每个线程分配了span的缓存, 这个缓存即是mcache,代码位于 runtime/mcache.go:

const numSpanClasses = 136
type mcache struct {
    // 微对象分配器相关
    tiny       uintptr
    tinyoffset uintptr
    tinyAllocs uintptr
    
    // mcache 中缓存的 mspan,每种 spanClass 各一个
    alloc [numSpanClasses]*mspan 
    // ...
}

(1)mcache 是每个 P 独有的缓存,因此交互无锁

(2)mcache 将每种 spanClass 等级的 mspan 各缓存了一个,总数为 2(nocan 维度) * 68(大小维度)= 136

(3)mcache 中还有一个为对象分配器 tiny allocator,用于处理小于 16B 对象的内存分配。
在这里插入图片描述

中心缓存 mcentral

mcache作为线程的私有资源为单个线程服务, 而central则是全局资源, 为多个线程服务, 当某个线程内存不足时会向mcentral申请, 当某个线程释放内存时又会回收进mcentral。代码位于 runtime/mcentral.go

type mcentral struct {
    // 对应的 spanClass
    spanclass spanClass
    // 有空位的 mspan 集合,数组长度为 2 是用于抗一轮 GC
    partial [2]spanSet 
    // 无空位的 mspan 集合
    full    [2]spanSet 
}

(1)每个 mcentral 对应一种 spanClass

(2)每个 mcentral 下聚合了该 spanClass 下的 mspan

(3)mcentral 下的 mspan 分为两个链表,分别为有空间 mspan 链表 partial 和满空间 mspan 链表 full

(4)每个 mcentral 一把锁

全局堆缓存 mheap

从mcentral数据结构可见, 每个mcentral对象只管理特定的class规格的span。 事实上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap数据结构中。代码位于 runtime/mheap.go

type mheap struct {
    // 堆的全局锁
    lock mutex


    // 空闲页分配器,底层是多棵基数树组成的索引,每棵树对应 16 GB 内存空间
    pages pageAlloc 


    // 记录了所有的 mspan. 需要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的
    allspans []*mspan


    // heapAreana 数组,64 位系统下,二维数组容量为 [1][2^22]
    // 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22*64M = 256T
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena


    // ...
    // 多个 mcentral,总个数为 spanClass 的个数
    central [numSpanClasses]struct {
        mcentral mcentral
        // 用于内存地址对齐
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }


    // ...
}
  • 对于 Golang 上层应用而言,堆是操作系统虚拟内存的抽象
  • 以页(8KB)为单位,作为最小内存存储单元
  • 负责将连续页组装成 mspan
  • 全局内存基于 bitMap 标识其使用情况,每个 bit 对应一页,为 0 则自由,为 1 则已被 mspan 组装
  • 通过 heapArena 聚合页,记录了页到 mspan 的映射信息(2.7小节展开)
  • 建立空闲页基数树索引 radix tree index,辅助快速寻找空闲页(2.6小节展开)
  • 是 mcentral 的持有者,持有所有 spanClass 下的 mcentral,作为自身的缓存
  • 内存不够时,向操作系统申请,申请单位为 heapArena(64M)

内存分配流程

tiny对象申请

对于tiny对象的申请,mcache中有专门的内存区域“tiny”来进行特殊处理。“tiny”将对象按大小与tinyoffset(“tiny”当前分配地址)对齐,然后分配,并记录下新的tinyoffset,用于下次分配。如果空间不足,则另外申请16 byte的内存块。

noscan := typ == nil || typ.ptrdata == 0
    // ...
        if noscan && size < maxTinySize {
        // tiny 内存块中,从 offset 往后有空闲位置
          off := c.tinyoffset
          // ...
            // 如果当前 tiny 内存块空间还够用,则直接分配并返回
            if off+size <= maxTinySize && c.tiny != 0 {
            // 分配空间
                x = unsafe.Pointer(c.tiny + off)
                c.tinyoffset = off + size
                c.tinyAllocs++
                mp.mallocing = 0
                releasem(mp)
                return x
            }
           // ...
        }

mcache 分配

// 根据对象大小,映射到其所属的 span 的等级(0~66)
          var sizeclass uint8
          // get size class ....     
          // 对应 span 等级下,分配给每个对象的空间大小(0~32KB)
          // get span class
          spc := makeSpanClass(sizeclass, noscan) 
          // 获取 mcache 中的 span
          span = c.alloc[spc]  
          // 从 mcache 的 span 中尝试获取空间        
          v := nextFreeFast(span)
          if v == 0 {
          // mcache 分配空间失败,则通过 mcentral、mheap 兜底            
             v, span, shouldhelpgc = c.nextFree(spc)
          }     
          // 分配空间  
          x = unsafe.Pointer(v)

在 mspan 中,基于 Ctz64 算法,根据 mspan.allocCache 的 bitMap 信息快速检索到空闲的 object 块,进行返回.

代码位于 runtime/malloc.go 文件中:

func nextFreeFast(s *mspan) gclinkptr {
    // 通过 ctz64 算法,在 bit map 上寻找到首个 object 空位
    theBit := sys.Ctz64(s.allocCache) 
    if theBit < 64 {
        result := s.freeindex + uintptr(theBit)
        if result < s.nelems {
            freeidx := result + 1
            if freeidx%64 == 0 && freeidx != s.nelems {
                return 0
            }
            s.allocCache >>= uint(theBit + 1)
            // 偏移 freeindex 
            s.freeindex = freeidx
            s.allocCount++
            // 返回获取 object 空位的内存地址 
            return gclinkptr(result*s.elemsize + s.base())
        }
    }
    return 0
}

mcentral 分配

当 mspan 无可用的 object 内存块时,会步入 mcache.nextFree 方法进行兜底.

代码位于 runtime/mcache.go 文件中:

func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
    s = c.alloc[spc]
    // ...
    // 从 mcache 的 span 中获取 object 空位的偏移量
    freeIndex := s.nextFreeIndex()
    if freeIndex == s.nelems {
        // ...
        // 倘若 mcache 中 span 已经没有空位,则调用 refill 方法从 mcentral 或者 mheap 中获取新的 span    
        c.refill(spc)
        // ...
        // 再次从替换后的 span 中获取 object 空位的偏移量
        s = c.alloc[spc]
        freeIndex = s.nextFreeIndex()
    }
    // ...
    v = gclinkptr(freeIndex*s.elemsize + s.base())
    s.allocCount++
    // ...
    return
}

倘若 mcache 中,对应的 mspan 空间不足,则会在 mcache.refill 方法中,向更上层的 mcentral 乃至 mheap 获取 mspan,填充到 mache 中:

代码位于 runtime/mcache.go 文件中:

func (c *mcache) refill(spc spanClass) {  
    s := c.alloc[spc]
    // ...
    // 从 mcentral 当中获取对应等级的 span
    s = mheap_.central[spc].mcentral.cacheSpan()
    // ...
    // 将新的 span 添加到 mcahe 当中
    c.alloc[spc] = s
}

mcentral.cacheSpan 方法中,会加锁(spanClass 级别的 sweepLocker),分别从 partial 和 full 中尝试获取有空间的 mspan:

代码位于 runtime/mcentral.go 文件中:

func (c *mcentral) cacheSpan() *mspan {
    // ...
    var sl sweepLocker    
    // ...
    sl = sweep.active.begin()
    if sl.valid {
        for ; spanBudget >= 0; spanBudget-- {
            s = c.partialUnswept(sg).pop()
            // ...
            if s, ok := sl.tryAcquire(s); ok {
                // ...
                sweep.active.end(sl)
                goto havespan
            }
            
        // 通过 sweepLock,加锁尝试从 mcentral 的非空链表 full 中获取 mspan
        for ; spanBudget >= 0; spanBudget-- {
            s = c.fullUnswept(sg).pop()
           // ...
            if s, ok := sl.tryAcquire(s); ok {
                // ...
                sweep.active.end(sl)
                goto havespan
                }
                // ...
            }
        }
        // ...
    }
    // ...


    // 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了
havespan:
    // ...
    return
}

mheap 分配

在 mcentral.cacheSpan 方法中,倘若从 partial 和 full 中都找不到合适的 mspan 了,则会调用 mcentral 的 grow 方法,将事态继续升级:

func (c *mcentral) cacheSpan() *mspan {
    // ...
    // mcentral 中也没有可用的 mspan 了,则需要从 mheap 中获取,最终会调用 mheap_.alloc 方法
    s = c.grow()
   // ...


    // 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了
havespan:
    // ...
    return
}

经由 mcentral.grow 方法和 mheap.alloc 方法的周转,最终会步入 mheap.allocSpan 方法中:

func (c *mcentral) grow() *mspan {
    npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
    size := uintptr(class_to_size[c.spanclass.sizeclass()])


    s := mheap_.alloc(npages, c.spanclass)
    // ...


    // ...
    return s
}

代码位于 runtime/mheap.go

func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
    var s *mspan
    systemstack(func() {
        // ...
        s = h.allocSpan(npages, spanAllocHeap, spanclass)
    })
    return s
}

代码位于 runtime/mheap.go

func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) {
    gp := getg()
    base, scav := uintptr(0), uintptr(0)
    
    // ...此处实际上还有一阶缓存,是从每个 P 的页缓存 pageCache 中获取空闲页组装 mspan,此处先略去了...
    
    // 加上堆全局锁
    lock(&h.lock)
    if base == 0 {
        // 通过基数树索引快速寻找满足条件的连续空闲页
        base, scav = h.pages.alloc(npages)
        // ...
    }
    
    // ...
    unlock(&h.lock)


HaveSpan:
    // 把空闲页组装成 mspan
    s.init(base, npages)
    
    // 将这批页添加到 heapArena 中,建立由页指向 mspan 的映射
    h.setSpans(s.base(), npages, s)
    // ...
    return s
}

向操作系统申请

倘若 mheap 中没有足够多的空闲页了,会发起 mmap 系统调用,向操作系统申请额外的内存空间.

代码位于 runtime/mheap.go 文件的 mheap.grow 方法中:

func (h *mheap) grow(npage uintptr) (uintptr, bool) {
    av, asize := h.sysAlloc(ask)
}
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
       v = sysReserve(unsafe.Pointer(p), n)
}
func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {
    return sysReserveOS(v, n)
}
func sysReserveOS(v unsafe.Pointer, n uintptr) unsafe.Pointer {
    p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
    if err != 0 {
        return nil
    }
    return p
}

mallocgc源码解读

代码位于 runtime/malloc.go 文件中:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // ...    
    // 获取 m
    mp := acquirem()
    // 获取当前 p 对应的 mcache
    c := getMCache(mp)
    var span *mspan
    var x unsafe.Pointer
    // 根据当前对象是否包含指针,标识 gc 时是否需要展开扫描
    noscan := typ == nil || typ.ptrdata == 0
    // 是否是小于 32KB 的微、小对象
    if size <= maxSmallSize {
    // 小于 16 B 且无指针,则视为微对象
        if noscan && size < maxTinySize {
        // tiny 内存块中,从 offset 往后有空闲位置
          off := c.tinyoffset
          // 如果大小为 5 ~ 8 B,size 会被调整为 8 B,此时 8 & 7 == 0,会走进此分支
          if size&7 == 0 {
                // 将 offset 补齐到 8 B 倍数的位置
                off = alignUp(off, 8)
                // 如果大小为 3 ~ 4 B,size 会被调整为 4 B,此时 4 & 3 == 0,会走进此分支  
           } else if size&3 == 0 {
           // 将 offset 补齐到 4 B 倍数的位置
                off = alignUp(off, 4)
                // 如果大小为 1 ~ 2 B,size 会被调整为 2 B,此时 2 & 1 == 0,会走进此分支  
           } else if size&1 == 0 {
            // 将 offset 补齐到 2 B 倍数的位置
                off = alignUp(off, 2)
           }
// 如果当前 tiny 内存块空间还够用,则直接分配并返回
            if off+size <= maxTinySize && c.tiny != 0 {
            // 分配空间
                x = unsafe.Pointer(c.tiny + off)
                c.tinyoffset = off + size
                c.tinyAllocs++
                mp.mallocing = 0
                releasem(mp)  
                return x
            } 
            // 分配一个新的 tiny 内存块
            span = c.alloc[tinySpanClass]    
            // 从 mCache 中获取
            v := nextFreeFast(span)        
            if v == 0 {
            // 从 mCache 中获取失败,则从 mCentral 或者 mHeap 中获取进行兜底
                v, span, shouldhelpgc = c.nextFree(tinySpanClass)
            }   
// 分配空间      
            x = unsafe.Pointer(v)
           (*[2]uint64)(x)[0] = 0
           (*[2]uint64)(x)[1] = 0
           size = maxTinySize
        } else {
          // 根据对象大小,映射到其所属的 span 的等级(0~66)
          var sizeclass uint8
          if size <= smallSizeMax-8 {
              sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
          } else {
              sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
          }        
          // 对应 span 等级下,分配给每个对象的空间大小(0~32KB)
          size = uintptr(class_to_size[sizeclass])
          // 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描
          spc := makeSpanClass(sizeclass, noscan) 
          // 获取 mcache 中的 span
          span = c.alloc[spc]  
          // 从 mcache 的 span 中尝试获取空间        
          v := nextFreeFast(span)
          if v == 0 {
          // mcache 分配空间失败,则通过 mcentral、mheap 兜底            
             v, span, shouldhelpgc = c.nextFree(spc)
          }     
          // 分配空间  
          x = unsafe.Pointer(v)
          // ...
       }      
       // 大于 32KB 的大对象      
   } else {
       // 从 mheap 中获取 0 号 span
       span = c.allocLarge(size, noscan)
       span.freeindex = 1
       span.allocCount = 1
       size = span.elemsize         
       // 分配空间   
        x = unsafe.Pointer(span.base())
   }  
   // ...
   return x
}

内存分配过程总结

针对待分配对象的大小不同有不同的分配逻辑:

  • (0, 16B) 且不包含指针的对象: Tiny分配
  • (0, 16B) 包含指针的对象: 正常分配
  • [16B, 32KB] : 正常分配
  • (32KB, -) : 大对象分配其中Tiny分配和大对象分配都属于内存管理的优化范畴, 这里暂时仅关注一般的分配方法。

以申请size为n的内存为例, 分配步骤如下:

  1. 获取当前线程的私有缓存mcache
  2. 跟据size计算出适合的class的ID
  3. 从mcache的alloc[class]链表中查询可用的span
  4. 如果mcache没有可用的span则从mcentral申请一个新的span加入mcache中
  5. 如果mcentral中也没有可用的span则从mheap中申请一个新的span加入mcentral
  6. 从该span中获取到空闲对象地址并返回

编程小Tips

首先看下哪些对象会分配到堆上?

  1. 堆上的指针只能指向堆上的对象
  2. interface、chan、map所持有或转化为interface的对象都会在堆上
  3. 函数内返回指针类型的,指针指向的对象会逃逸到堆上。
  4. string类型一定在堆上
  5. 切片扩容和拷贝

原则上,减少GC就是减少在堆上分配对象。

  1. 切片和map知道大小的,预先分配足够的大小,避免扩容。

  2. 尽量少生成string,使用切片的方式复用string。

  3. 少用反射,被反射的对象都会分配到堆上。

  4. 使用sync.Pool复用对象,避免重复生产销毁对象。

  5. 尽量让chan传递较小的对象,所有chan传递的对象都会分配到堆上。

第二个方法:调节GC频率

通过debug.SetGCPercent或环境变量GOGC设置堆空间增长率p,只有当堆空间达到上次GC完成时堆空间大小*(1+p)才会触发GC。

通过设置debug.SetMemoryLimit或环境变量GOMEMLIMIT设置软限制内存限制,这个条件会使go的GC频率动态调节,用于达到GOMEMLIMIT的需要,可用于防止OOM。但不会强制程序的内存使用量。

参考文档

Golang 内存模型与分配机制 - 小徐先生的文章 - 知乎
go 内存管理和GC详解 - 卡萨布兰卡的文章 - 知乎
操作系统虚拟内存管理(二):虚拟内存管理 - Mulily的文章 - 知乎
Golang内存管理(一):堆内存管理 - Mulily的文章 - 知乎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1642376.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

牛客热题:两个链表的第一个公共节点

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;力扣刷题日记 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 文章目录 牛客热题&#xff1a;两个链表的第一个公共节点题目链…

【Linux】进程的隔离和控制:namespace 隔离、cgroup 控制

文章目录 五、namespace 隔离dd -- 读取、转换并输出数据mkfs -- 格式化文件系统df -- 显示文件系统磁盘使用情况mount -- 加载文件系统到指定的加载点unshare -- 创建子进程&#xff0c;同时与父程序不共享namespace一个 demo 六、cgroup(Control Group) 相关命令pidstat -- 监…

中药辨别二

声明&#xff1a;参考懒兔子公益课&#xff0c;参考网络资料和部分网络图片整理而成&#xff0c;仅供学习使用&#xff0c;不提供商业活动价值&#xff0c;文章描述的中药仅供学习&#xff0c;请在专业医师或专业医生指导下使用药材&#xff0c;擅自或其他情况下使用&#xff0…

使用Python实现二维码生成工具

二维码的本质是什么&#xff1f; 二维码本质上&#xff0c;就是一段字符串。 我们可以把任意的字符串&#xff0c;制作成一个二维码图片。 生活中使用的二维码&#xff0c;更多的是一个 URL 网址。 需要用到的模块 先看一下Python标准库&#xff0c;貌似没有实现这个功能的…

2024网络安全面试问题宝典(4万字)

2024网络安全厂商面试问题宝典(4万字) 目录 评分标准网络基础问题 TCP建立连接要进行3次握手&#xff08;syn-syn&#xff0c;ack-ack&#xff09;&#xff0c;而断开连接要进行4次&#xff08;fin-ack-fin-ack&#xff09;TCP&#xff0c;UDP区别&#xff1a;安全常用的协议…

ZFile-开源在线网盘(列目录)程序,可以将你各个存储类型的存储源,统一到一个网页中查看、预览、维护

ZFile 是什么&#xff1f; ZFile 是一个适用于个人的在线网盘(列目录)程序&#xff0c;可以将你各个存储类型的存储源&#xff0c;统一到一个网页中查看、预览、维护&#xff0c;再也不用去登录各种各样的网页登录后管理文件&#xff0c;现在你只需要在 ZFile 中使用。你只需要…

排序算法之堆排序

首先在了解堆排序之前我们先来回顾一下什么叫做堆吧&#xff01; 基本概念 堆&#xff08;Heap&#xff09;&#xff1a;是一种特殊的完全二叉树&#xff0c;其中每个节点的值都大于或等于&#xff08;大顶堆&#xff09;或小于或等于&#xff08;小顶堆&#xff09;其子节点的…

Vue 组件间的数据绑定

在Vue组件中&#xff0c;v-model指令可以用来实现双向数据绑定。它用于将组件的属性和父组件中的数据进行双向绑定&#xff0c;使得当属性的值改变时&#xff0c;父组件中的数据也会相应地改变&#xff0c;并且当父组件中的数据改变时&#xff0c;属性的值也会相应地改变。 目…

国内各种免费AI聊天机器人(ChatGPT)推荐(中)

作者主页&#xff1a;点击&#xff01; 国内免费AI推荐(ChatGPT)专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月29日15点20分 随着人工智能技术的不断发展&#xff0c;AI聊天机器人已经逐渐融入我们的日常生活。它们可以提供各种服务&#xff0c;例如聊天、…

python爬虫实战

import requests import json yesinput(输入页数&#xff1a;) yesint(yes)headers {"accept": "application/json, text/plain, */*","accept-language": "zh-CN,zh;q0.9","content-type": "application/json",…

文件同步--Resilio Sync

近期有很多文本处理工作&#xff0c;每次通过社交软件发来发去&#xff0c;比较麻烦&#xff0c;还是希望能够自动同步&#xff0c;方便快捷&#xff0c;然后师弟就给我推荐了Resilio Sync&#xff0c;目前配置完毕&#xff0c;使用下来感受良好&#xff0c;mark一下。 特点 跨…

喝汽水问题

问题描述&#xff1a; 喝汽水&#xff0c;1瓶汽水1元&#xff0c;2个空瓶可以换一瓶汽水&#xff0c;给20元&#xff0c;可以喝多少汽水&#xff08;编程实现&#xff09;。解题思路&#xff1a; 变量解释&#xff1a;money总金额数&#xff0c;price汽水单价&#xff0c;excha…

环形列表 题目分析

一、题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 二、思路 1.使用快慢指针&#xff0c;快指针一次走两步&#xff0c;慢指针一次走一步&#xff0c;二者相对速度为1 2.当慢指针刚好进入环时&#xff0c;设二者的相对距离为N 3.如果链表中存在环&#xff…

智能私信神器,转化率飙升的秘密!

在当今信息爆炸的时代&#xff0c;企业和商家面临着巨大的竞争压力&#xff0c;如何有效地吸引潜在客户、提高客户转化率成为摆在每一位市场营销人员面前的难题。随着人工智能技术的不断发展&#xff0c;智能私信软件应运而生&#xff0c;为企业提供了一个高效、便捷的解决方案…

【OpenNJet下一代云原生之旅】

OpenNJet下一代云原生之旅 1、OpenNJet的定义OpenNJet架构图 2、OpenNJet的特点性能无损动态配置灵活的CoPilot框架支持HTTP/3支持国密企业级应用高效安全 3、OpenNJet的功能特性4、OpenNJet的安装使用编译安装配置yum源创建符号连接修改配置编译 5、通过 OpenNJet 部署 WEB SE…

[CISCN 2018]sm

目录 1.题目 3.解题 4.参考 1.题目 题目链接 from Crypto.Util.number import getPrime,long_to_bytes,bytes_to_long from Crypto.Cipher import AES import hashlib from random import randint def gen512num():order[]while len(order)!512:tmprandint(1,512)if tmp n…

探索小猪APP分发平台:构建高效的应用推广之路

在当今快速发展的移动互联网时代探索小猪APP分发平台&#xff1a;构建高效的应用推广之路&#xff0c;应用分发成为连接开发者与用户的关键桥梁。一个高效的分发平台可以显著提升应用的可达性和用户增长速度。 小猪app分发zixun.ppzhu.net 引言&#xff1a;小猪APP分发平台简介…

Spring AI 抢先体验,5 分钟玩转 Java AI 应用开发

作者&#xff1a;刘军 Spring AI 是 Spring 官方社区项目&#xff0c;旨在简化 Java AI 应用程序开发&#xff0c;让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。 Spring Cloud Alibaba AI 以 Spring AI 为基础&#xff0c;并在此基础上提供阿里云通义系列大模…

远程桌面报错:【出现验证错误。要求的函数不受支持】

WinR 输入【gpedit.msc】回车 依次打开 计算机配置----管理模板-----系统-----凭据分配---加密数据库修正 选择【已启用】&#xff0c;下拉菜单选择【易受攻击】

phpMyAdmin增加自定义IP登录教程

phpMyAdmin增加自定义IP登录教程 1、打开phpMyAdmin目录&#xff0c; 在此目录下是否有config.sample.inc.php文件&#xff0c;如果存在&#xff0c;那么将其改名为config.inc.php&#xff08;为避免修改失误所造成的损失&#xff0c;强烈建议先备份config.sample.inc.php文件…