Golang内存模型与源码解析

news/2024/12/19 22:14:34/文章来源:https://www.cnblogs.com/MelonTe/p/18618032

0、引言

本篇笔记用于记录作者在学习Golang的GC模型之前,对Golang内存模型的学习。目前使用的Go版本为1.22.4

1、Golang内存管理宏观结构

假设我们每次向内存池申请空间时,都需要频繁地向操作系统发出请求,这不仅会增加内存分配的时间,还可能引入竞争和锁的开销,从而导致性能瓶颈。尤其是在多线程并发的开发场景下,这样的问题带来的损耗是显而易见的。为了减少这种开销,我们不妨在初始化内存空间时,一次性地向操作系统申请多一点的空间,只有当现有空间不足的时候,再次向操作系统申请新的空间。通过这种方式,可以有效减少内存分配过程中多线程并发带来的竞争,提高程序的性能。Go语言的内存管理模型正是围绕着高效的内存分配机制和垃圾回收机制来优化这类问题的,从而在大规模并发应用中取得更好的性能表现。

Golang的内存管理结构宏观图如下:

设计到的核心数据结构有:

  • mheap:Golang内存模型中最大的内存池,是全局的内存起源,它直接和操作系统进行内存申请交互,向mheap申请内存需要持有锁。
  • mcentral:mheap的粒度细化的内存池,存在于mheap中,总数量为136(Span Class)个。
  • mcache:每个P持有的一份本地内存缓存,访问其不需要持有锁。

接下来我们来详细了解需要接触到的相关概念。

2、内存管理模型相关概念及源码解析

2.1、page

借鉴操作系统内存分页管理的思想,Golang的内存管理模型也存在着Page,其是内存管理模型和操作系统内存交互的最小单元,大小为8KB,对于Golang来说,操作系统的虚拟内存就是被划分成N个Page的大内存池。

2.2、mspan

多个连续的page被称之为mspan,其大小为8KB~32KB。其根据分配object大小来划分可以划分为67种。

其源码的核心字段如下:

type mspan struct {//标识前后mspan的指针next *mspan    prev *mspan     //起始地址startAddr uintptr//包含的页数npages    uintptr // freeindex 是一个槽索引,范围在 0 到 nelems 之间,表示开始扫描该 span 中下一个空闲对象的位置。// 每次分配都会从 freeindex 开始扫描 allocBits,直到遇到一个 0,表示找到一个空闲对象。// 随后,freeindex 会调整为刚发现的空闲对象之后的位置,以便下次扫描从新的位置开始。//// 如果 freeindex == nelem,表示这个 span 中没有空闲对象。freeindex uint16//该span中的object的数量nelems uint16//是 allocBits 的部分缓存,且保存的是 allocBits 的补码。allocCache uint64//mspan的等级spanclass             spanClass     // size class and noscan (uint8)
}
  • nextprev用于指向同规格下的上一mspan与下一mspan,将整条mspan封装成链表,有助于扩展和销毁。
  • startAddr用于记录起始地址。
  • nelems用于记录当前mspan中object的数量。
  • freeindex用于标识下一次扫描寻找object的位置,在该位置前的object都已经被使用。

2.3、object

object是协程应用逻辑一次向Golang申请的对象。objectgolang内存管理模型针对内存分配更加细化的内存管理单元,一个mspan在初始化时会被划分为多个object。例如一个大小为8B的object归属于大小为8KB的mspan,该mspan被划分为1024个object。object根据大小可以从8B~32KB划分为67种。golang内存管理内部本身用来给对象存储内存的基本单元是object

下图可以展示object、page、mspan三者间的关系。

2.4、SizeClass与SpanClass

SizeClass是针对Object的大小来进行划分的等级,标识着每次申请空间的容量对应着哪一个等级。例如一次内存请求中,申请获得1B~7B之间的容量,那都归属于SizeClass 1级别。

SpanClass是针对mspan来划分的,指span大小的级别。(虽然mspan的大小只能为page的整数倍,最高只能为32KB,但是因为一个mspan可以被不同大小的object划分,因此mspan具有多个种类)。一个SizeClass对应着两个SpanClass,其中一个SpanClass为存放需要GC扫描的对象,而另一个则存放不需要GC扫描的对象。

其对应关系图可以用下图来表示。

通过sizeclass生成spanclass的源码如下:

type spanClass uint8// 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
}

生成规则为现将sizeclass左移一位,即乘2,最小位标识是否为noscan。

2.5、mcache

mcache被一个P持有,作为其本地缓存,当运行在和当前P绑定的线程上,需要申请内存资源时,会优先从mcache上获得,因为一个P在同一时刻只能有一个M在其上运行,因此访问mcache不需要持有锁,加快了内存分配。

mcache在初始化时,持有每一种spanclass的一个mspan实体,不同spanclassMspan长度会不同

其源码的核心字段如下:

type mcache struct {//微对象分配器tiny       uintptrtinyoffset uintptrtinyAllocs uintptr//缓存的mspan,每一种spanclass有一个mspanalloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass}

mcache总的tiny字段用于处理小于16B对象的内存分配,将会在下文提及。

2.6、mcentral

mcentral作为中心缓存,起到分配小对象空间分配的作用,当mcache中的mspan空间不足的时候,就会尝试向mcentral获取一份mspan进行补充。有多少个spanclass等级,就存在着多少个mcentral,每一个mcentral只负责自己等级的mspan分配。

核心字段如下:

type mcentral struct {spanclass spanClass//维护全部空闲的span集合partial [2]spanSet //维护存在非空闲的span集合full    [2]spanSet 
}

mcentral持有两个mspan集合,一个集合用于存放含有可用空间的mspanpartial集合,另一个则存放没有可用空间的mspanfull集合。每一个集合长度为2,是因为有一条用于处理GC。

2.7、mheap

对于golang的上层应用而言,mheap就是它们眼中的操作系统虚拟内存,通过向mheap申请内存而不是每次都向操作系统申请开辟空间,可以减少其开销。mheap的上游就是mcentral,当mcentral的内存不够时,就会以page为单位向mheap请求空间,而当mheap的空间不够时,则会向下游的操作系统申请空间,申请的单位为64M

type mheap struct {// 堆的全局锁lock mutex// 空闲页分配器pages pageAlloc // 记录了所有的 mspan. 需要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的allspans []*mspan// heapAreana 数组,64 位系统下,二维数组容量为 [1][2^22]// 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22*64M = 256Tarenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena// ...// 多个 mcentral,总个数为 spanClass 的个数central [numSpanClasses]struct {mcentral mcentral// 用于内存地址对齐pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte}// ...
}

3、内存分配流程

不同的层次之间申请内存的单位如下图所示:

根据每次申请的内存的大小来划分,可以分为三种情况:

  • 在(0,16B)之间,进入tiny对象分配流程
  • 在[16B,32KB]之间,进入小对象分配流程
  • 大于32KB,进入大对象分配流程

三种情况的具体流程都在mallocgc函数中有具体的体现,其具体遵循以下的步骤:

  • tiny对象分配:若申请内存大小为0B,则直接返回一个表示空字节的地址,该地址在程序初始化的时候就将确定不会发生改变;若申请内存大小为(0,16B],且不包含指针对象,则进入微对象分配流程,尝试从本地mcache缓存中的tiny分配器中获取内存,若内存不足,则会进入到向mcentral申请sizeclass为2的mspan的流程,将获取到的mspan补充到mcache,然后再重新获得tiny内存;若mcentral也不足,则会向mheap申请page补充到mcentral,再进入之前的步骤。
  • 小对象分配:与tiny对象分配类似,先根据object大小,找到对应的spanclass级别,在mcache查找该spanclass下的span是否还有容量,有则获取,否则向下游申请分配。
  • 大对象分配:P将直接约过mcachemcentral,向mheap获取指定的pages

3.1、源码一览

3.1.1、主流程mallocgc

mallocgc函数定位于runtime/mheap.go中,主体流程如下:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {//若请求的size为0,就返回一个固定的表示零空间的地址if size == 0 {return unsafe.Pointer(&zerobase)}//...//获取mmp := acquirem()//标识正在进行内存获取,防止被gc抢占mp.mallocing = 1//...//获取mcachec := getMCache(mp)var span *mspanvar header **_typevar x unsafe.Pointer//根据当前对象是否包含指针,标识gc时是否需要展开扫描noscan := typ == nil || typ.PtrBytes == 0//是否是<=32KB的小对象、微对象if size <= maxSmallSize-mallocHeaderSize {//小于16B并且没有指针,进入微对象分配if noscan && size < maxTinySize {//tiny内存块中,从off开始存在空闲空间off := c.tinyoffset//...//当前tiny块内存够用,则进行直接分配并且返回if off+size <= maxTinySize && c.tiny != 0 {//分配内存x = unsafe.Pointer(c.tiny + off)c.tinyoffset = off + sizec.tinyAllocs++mp.mallocing = 0releasem(mp)return x}//tiny空间不够,需要先申请。//tinyspanclass为5span = c.alloc[tinySpanClass]//尝试从mcache获取大小为16B的内存块,为0表示获取失败v := nextFreeFast(span)if v == 0 {//从mcentral、mheap获取兜底v, span, shouldhelpgc = c.nextFree(tinySpanClass)}//分配空间x = unsafe.Pointer(v)(*[2]uint64)(x)[0] = 0(*[2]uint64)(x)[1] = 0if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {c.tiny = uintptr(x)c.tinyoffset = size}size = maxTinySize} else {hasHeader := !noscan && !heapBitsInSpan(size)if goexperiment.AllocHeaders && hasHeader {size += mallocHeaderSize}//根据对象大小,映射其所属的span等级var sizeclass uint8if size <= smallSizeMax-8 {sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]} else {sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]}//分配给object空间的大小,0~32KBsize = uintptr(class_to_size[sizeclass])// 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描spc := makeSpanClass(sizeclass, noscan)//获取mcache的spanspan = c.alloc[spc]//尝试从该span中获取空间v := nextFreeFast(span)if v == 0 {//获取失败,尝试从mcentral、mheap获取v, span, shouldhelpgc = c.nextFree(spc)}x = unsafe.Pointer(v)//...}//大于32KB的大对象,直接尝试从mheap获取} else {//从mheap获取span = c.allocLarge(size, noscan)span.freeindex = 1span.allocCount = 1size = span.elemsizex = unsafe.Pointer(span.base())/...}return x
}

3.1.2、nextFreeFast

nextFreeFast用于快速从mspan中获取object

// nextFreeFast returns the next free object if one is quickly available.
// Otherwise it returns 0.
func nextFreeFast(s *mspan) gclinkptr {//寻找首个object空位,没有空闲对象则返回64theBit := sys.TrailingZeros64(s.allocCache) // Is there a free object in the allocCache?if theBit < 64 {result := s.freeindex + uint16(theBit)//确保该索引未超过该span的对象范围if result < s.nelems {//freeidx是新的freeindexfreeidx := result + 1//超出了nelemes数量,则返回0if freeidx%64 == 0 && freeidx != s.nelems {return 0}s.allocCache >>= uint(theBit + 1)s.freeindex = freeidxs.allocCount++//返回获取object空位的内存地址return gclinkptr(uintptr(result)*s.elemsize + s.base())}}return 0
}

3.1.3、nextFree

nextFree函数会首先尝试向mspan获取对应大小的object,若获取失败,则会向下游请求补充内存。

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获取一个spanclass级别的mspan,这个mspan会至少含有一个空的object。当前mcache中的span必须满了,才会调用此方法。
func (c *mcache) refill(spc spanClass) {  s := c.alloc[spc]// ...// 从 mcentral 当中获取对应等级的 spans = mheap_.central[spc].mcentral.cacheSpan()// ...// 将新的 span 添加到 mcahe 当中c.alloc[spc] = s
}
//从mcentral申请一个span,将被用在mcahce中。
func (c *mcentral) cacheSpan() *mspan {// ...var sl sweepLocker    // ...//尝试清扫和分配未清扫的mspansl = 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 中获取 mspanfor ; spanBudget >= 0; spanBudget-- {s = c.fullUnswept(sg).pop()// ...if s, ok := sl.tryAcquire(s); ok {// ...sweep.active.end(sl)goto havespan}// ...}}// ...}// ...// We failed to get a span from the mcentral so get one from mheap.s = c.grow()if s == nil {return nil}// 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了
havespan:// ...return
}

3.1.4、mcentral.grow

mcentral.grow方法用于mcentralmheap申请分配一个新的mspan

// Grow从堆中分配一个新的空span,并为c的size类初始化它。
func (c *mcentral) grow() *mspan {//确定需要的页数和大小npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])size := uintptr(class_to_size[c.spanclass.sizeclass()])//从堆中分配一个新的mspans := mheap_.alloc(npages, c.spanclass)//分配失败则返回nilif s == nil {return nil}//确定当前mspan可以容纳多少个对象n := s.divideByElemSize(npages << _PageShift)s.limit = s.base() + size*ns.initHeapBits(false)//分配成功return s
}
// alloc 从 GC 管理的堆中分配一个新的 npage 页的span。
//
// spanclass 指示span的大小类别和可扫描性。
//
// 返回一个已完全初始化的span。span.needzero 表示
// 该span是否已被置零。请注意,它可能并未被置零。
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {var s *mspansystemstack(func() {if !isSweepDone() {h.reclaim(npages)}//转入allocspan方法s = h.allocSpan(npages, spanAllocHeap, spanclass)})return s
}
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) {//初始化
gp := getg()base, scav := uintptr(0), uintptr(0)growth := uintptr(0)needPhysPageAlign := physPageAlignedStacks && typ == spanAllocStack && pageSize < physPageSize//如果不需要物理页对齐且页数小于缓存容量的四分之一,尝试使用 P 的局部页缓存。pp := gp.m.p.ptr()if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 {c := &pp.pcache// If the cache is empty, refill it.if c.empty() {lock(&h.lock)*c = h.pages.allocToCache()unlock(&h.lock)}base, scav = c.alloc(npages)if base != 0 {s = h.tryAllocMSpan()if s != nil {goto HaveSpan}}}//局部缓存分配失败,则锁住全局堆进行分配lock(&h.lock)//....//从全局堆分配if base == 0 {// Try to acquire a base address.base, scav = h.pages.alloc(npages)//如果堆空间不足,则触发堆增长if base == 0 {var ok boolgrowth, ok = h.grow(npages)if !ok {unlock(&h.lock)return nil}base, scav = h.pages.alloc(npages)if base == 0 {throw("grew heap, but no adequate free space found")}}}if s == nil {// We failed to get an mspan earlier, so grab// one now that we have the heap lock.s = h.allocMSpanLocked()}unlock(&h.lock)//...HaveSpan:// 把空闲页组装成 mspans.init(base, npages)// 将这批页添加到 heapArena 中,建立由页指向 mspan 的映射h.setSpans(s.base(), npages, s)// ...return s
}

3.1.5、mheap.grow

mheap.grow用于mheap向操作系统获取虚拟内存。

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 {//通过 mmap 向操作系统请求一块虚拟内存。p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)if err != 0 {return nil}return p
}

4、参考博客

Golang 内存模型与分配机制

[[Go三关-典藏版]一站式Golang内存管理洗髓经 - 知乎](https://zhuanlan.zhihu.com/p/572059278#:~:text=本文收录于 《Golang修养之路》Golang的内存管理及设计也是开发者需要了解的领域之一,要理解)

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图床试验

本文来自博客园,作者:Glowingfire,转载请注明原文链接:https://www.cnblogs.com/Glowingfire/p/18617999

一文搞定理解RPC

前言RPC概念RPC协议RPC组成RPC协议RPC框架RPC的优点RPC与HTTP的区别 前言 RPC的概念相信很多软件从业人员或多或少都接触过,从开发到测试都可能需要跟它打交道。 但是对于为什么要用RPC?RPC的优点是什么?RPC是什么原理?它跟HTTP有什么不同?相信并不是每个人都比较熟悉。 那…

全场景一站式2024最新vmware环境下安装win7并且破解QTP

目录VMwareVMware和Ubuntu下载链接下载Win 7 系统各个操作系统网站激活码是什么查看是否激活激活操作vmware下安装ubuntu创建虚拟机下载VMtool灰色灰色按键点击不了下载提示有问题原因文件传递共享文件借助外界U盘有了VMTool就可以直接拖拽!!!!有了VMTool就可以全屏化——倒…

20222321 2024-2025-1 《网络与系统攻防技术》实验八实验报告

一.实验内容 (1)Web前端HTML 能正常安装、启停Apache。理解HTML,理解表单,理解GET与POST方法,编写一个含有表单的HTML。 (2)Web前端javascipt 理解JavaScript的基本功能,理解DOM。 在(1)的基础上,编写JavaScript验证用户名、密码的规则。在用户点击登陆按钮后回显“欢迎…

数量

技巧 比例型 出现一个比例,存在四种倍数关系倍数 你们有啥公因子,我也必须有尾数 出现乘法,分析个位,考虑尾数 。乘法中出现5和10尾数就确认了奇偶 与偶数相乘一定是偶数,与奇数相乘可能为偶数也可能为奇数拓展猜题 当 A = B*C ,求A ,考虑A的倍数 工程问题 利润问题 求最…

LVGL学习 - Visual Studio外部“.c.h”文件添加

LVGL项目工程添加“.c.h”文件后 “C1083”“LNK2019”报错的解决方法一、首先把文件添加至工程,现有项选择所需添加的“.c.h”文件但还是会有如下报错,解决方法在第2步。二、“.c”文件需要添加“extern "C"” 下图截至官方文档我试了只添加“extern "C"…

组合数学+ybt题解

加法原理 乘法原理 排列数 从 \(n\) 个数中任取 \(m\) 个元素的排列的方案数,表示为 \(A^m_n=\frac{n!}{(n-m)!}\) \(0!=1\) 全排列 \(A^n_n\) 组合数 从 \(n\) 个元素中取出 \(m\) 个元素的组合的个数,表示为 \(\dbinom{n}{m}= \frac{A^m_n}{m!}=\frac{n!}{m!(n-m)!}\) 如何…

苍穹外卖day02

JWT令牌、ThreadLocal、分页查询bug记录知识点记录新增员工新增员工需要填写创建人id和修改人id两个属性,这两个属性应该填本账户的id。 通过拦截器可以解析出JWT令牌中包含的登录员工id信息,但是该如何传递给Service的save方法? ThreadLocal并非一个Thread,而是Thread的局…

年底裁员开始了,大家做好准备吧!

各大互联网公司的接连裁员,政策限制的行业接连消失,让今年的求职雪上加霜,想躺平却没有资本,还有人说软件测试岗位饱和了,对此很多求职者深信不疑,因为投出去的简历回复的越来越少了。 另一面企业招人真的变得容易了吗?有企业HR吐槽,简历确实比以前多了好几倍,其实是变…

2024-2025-1 20241401 《计算机基础与程序设计》 第十三周学习总结

班级链接 2024计算机基础与程序设计作业要求 第十三周作业教材学习内容总结 《C语言程序设计》第12章结构体的定义和使用: 结构体类型的定义,以及结构体变量的创建和使用。结构体允许将不同数据类型的成员组合成一个整体,以便于管理和引用。 结构体变量的初始化: 结构体变量…