首页 > 系统相关 >Golang内存模型与源码解析

Golang内存模型与源码解析

时间:2024-12-19 22:20:19浏览次数:19  
标签:mspan object mcentral Golang 源码 内存 span mheap

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       uintptr
	tinyoffset uintptr
	tinyAllocs uintptr

	//缓存的mspan,每一种spanclass有一个mspan
	alloc [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 = 256T
    arenas [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)
	}

	//...

	//获取m
	mp := acquirem()
	//标识正在进行内存获取,防止被gc抢占
	mp.mallocing = 1

	//...
    //获取mcache
	c := getMCache(mp)
	var span *mspan
	var header **_type
	var 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 + size
				c.tinyAllocs++
				mp.mallocing = 0
				releasem(mp)
				return x
			}
			//tiny空间不够,需要先申请。
            //tinyspanclass为5
			span = 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] = 0
			if !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 uint8
			if size <= smallSizeMax-8 {
				sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
			} else {
				sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
			}
            //分配给object空间的大小,0~32KB
			size = uintptr(class_to_size[sizeclass])
            // 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描
			spc := makeSpanClass(sizeclass, noscan)
            //获取mcache的span
			span = 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 = 1
		span.allocCount = 1
		size = span.elemsize
		x = 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空位,没有空闲对象则返回64
	theBit := 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是新的freeindex
			freeidx := result + 1
            //超出了nelemes数量,则返回0
			if freeidx%64 == 0 && freeidx != s.nelems {
				return 0
			}
			s.allocCache >>= uint(theBit + 1)
			s.freeindex = freeidx
			s.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 当中获取对应等级的 span
    s = mheap_.central[spc].mcentral.cacheSpan()
    // ...
    // 将新的 span 添加到 mcahe 当中
    c.alloc[spc] = s
}
//从mcentral申请一个span,将被用在mcahce中。
func (c *mcentral) cacheSpan() *mspan {
    // ...
    var sl sweepLocker    
    // ...
    //尝试清扫和分配未清扫的mspan
    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
                }
                // ...
            }
        }
        // ...
    }
    // ...
	// 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()])
	//从堆中分配一个新的mspan
	s := mheap_.alloc(npages, c.spanclass)
    //分配失败则返回nil
	if s == nil {
		return nil
	}

	//确定当前mspan可以容纳多少个对象
	n := s.divideByElemSize(npages << _PageShift)
	s.limit = s.base() + size*n
	s.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 *mspan
	systemstack(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 bool
			growth, 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:
    // 把空闲页组装成 mspan
    s.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的内存管理及设计也是开发者需要了解的领域之一,要理解)

标签:mspan,object,mcentral,Golang,源码,内存,span,mheap
From: https://www.cnblogs.com/MelonTe/p/18618032

相关文章

  • Java+Vue的物流仓储管理系统(源码+文档)
    前言:物流仓储管理系统是一个集成了信息技术、仓储技术、物流技术等手段的综合性管理系统,旨在提高仓储运作效率、降低成本、优化资源配置。以下是对系统的八大管理模块的详细解释:一、车辆管理车辆管理模块主要负责物流运输车辆的调度、跟踪、维护和成本控制。它包括:车辆调度......
  • ssm公交查询系统12e14--程序+源码+数据库+调试部署+开发环境
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景随着城市交通的日益繁忙,公交系统作为城市交通的重要组成部分,承担着大量的出行需求。然而,传统的公交查询方式存在信息更新不及时、查询......
  • ssm个性化音乐系统658nv(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、研究背景与意义随着数字音乐产业的快速发展,人们对音乐的个性化需求日益增长。传统的音乐播放方式已无法满足用户对音乐内容的个性化追求。因此......
  • ssm毕设企业在线产品数据录入系统源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景在当今信息化高速发展的时代,企业数据管理已成为提升企业竞争力的关键因素之一。关于企业在线产品数据录入系统的研究,现有研究主要集中在大型ERP(企业......
  • ssm毕设企业招聘系统源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景在当今竞争激烈的市场环境中,企业招聘系统作为连接求职者与企业的桥梁,其重要性日益凸显。关于企业招聘系统的研究,现有文献主要集中在系统设计与开发、......
  • golang的互斥锁和读写锁
    golangmutex(互斥锁)1.锁最本质的作用保证原子性2.mutex使用原则适用于并发编程,尽量减少加锁区域的逻辑3.mutex的局限性仅限于单个进程内操作sema(信号量,semaphore的简称)是一种用于并发控制的机制资源计数:信号量维护一个资源计数。这个计数表示当前可用的资源数量获......
  • Dubbo 3.x源码(27)—Dubbo服务引用源码(10)subscribeURLs订阅应用级服务url
    基于Dubbo3.1,详细介绍了Dubbo服务的发布与引用的源码。上文我们学习了Dubbo3的应用级服务发现订阅refreshServiceDiscoveryInvoker方法的源码,下面我们将会学习应用级服务发现订阅后半部分的源码,即在获取到服务应用名之后,通过subscribeURLs方法进行应用级别的服务url订阅......
  • Java+Vue构建的ERP管理系统(源码+文档)
    前言:ERP管理系统是一种集成化的企业管理软件,旨在帮助企业优化资源配置、提升运营效率。以下是对ERP管理系统中各个模块的详细解释:一、零售管理零售管理模块主要处理与销售给最终消费者相关的业务。它包括:销售点(POS)系统,用于记录销售交易、管理库存和跟踪客户购买历史。客户......
  • 4、文件与内存转换相关
    4、文件与内存转换相关FileBufferToImageBuffer也是一样的长话短说。这里涉及了一点,就是内存对齐PE头与节区之间节区与节区时间会发生内存对齐。在文件中有一个文件对齐​​在可选PE头中有这两个进行标识,之前也写过这个内存对齐的博客,这里就不多说了下面贴几个代码模拟内......
  • 修改python jsonpickle源码, 实现不序列化对象私有成员(2)
    在前一篇文章中,通过修改源码来实现对象的私有成员不被序列化的功能.但是不够灵活,于是我又修改了一版,能够自定义过滤函数.实现起来不是很难,要点如下:#pickler.py的_flatten_obj_instance函数self._current_obj=obj#记录正在处理的对象returnself._flatten_dict......