Golang内存分配-笔记

1 span

golang将内存分成了从小到大的67个级别的span

span的划分方式是依据span中的元素大小,而不是一块span的大小。

所以当具体对象需要分配内存时,并不是直接分配span,而是分配不同级别的span中的元素

具体数据在golang的runtime包下的sizeclasses.go 文件中(数据太长不在此展示)

但是源码非常有必要一看,尤其是里面某些字段,重点看一下allocCache freeindexnelems

后面的源码重点的地方会涉及到这三个,如果不理解就可能看不不懂后面的源码(一些GC相关的字段已删去,太长了…)

type mspan struct {
	_    sys.NotInHeap
	next *mspan     // next span in list, or nil if none
	prev *mspan     // previous span in list, or nil if none
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // address of first byte of span aka s.base()
	npages    uintptr // number of pages in span

	manualFreeList gclinkptr // list of free objects in mSpanManual spans

	// freeindex is the slot index between 0 and nelems at which to begin scanning
	// for the next free object in this span.
	// Each allocation scans allocBits starting at freeindex until it encounters a 0
	// indicating a free object. freeindex is then adjusted so that subsequent scans begin
	// just past the newly discovered free object.
	//
	// If freeindex == nelem, this span has no free objects.
	//
	// allocBits is a bitmap of objects in this span.
	// If n >= freeindex and allocBits[n/8] & (1<<(n%8)) is 0
	// then object n is free;
	// otherwise, object n is allocated. Bits starting at nelem are
	// undefined and should never be referenced.
	//
	// Object n starts at address n*elemsize + (start << pageShift).
	freeindex uintptr
	// TODO: Look up nelems from sizeclass and remove this field if it
	// helps performance.
	nelems uintptr // number of object in the span.

	// Cache of the allocBits at freeindex. allocCache is shifted
	// such that the lowest bit corresponds to the bit freeindex.
	// allocCache holds the complement of allocBits, thus allowing
	// ctz (count trailing zero) to use it directly.
	// allocCache may contain bits beyond s.nelems; the caller must ignore
	// these.
	allocCache uint64
}

2 三级对象管理

go采用了TCmalloc的思想,采取了三级结构mcachemcentralmheap

2.1 mcache

  • 每个逻辑处理器P都存储了一个本地span缓存,叫做mcache

  • 协程需要内存可以直接冲mcache获取,因为同一时间只有一个协程运行在P上,所以这个过程不需要加锁。

  • mcache包中含有所有大小规格的mspan,但每一种都有一个,除了上面说到的0级span以外,mcache的span都来自mcentral。

2.2 mcentral

  • mcentral是被所有逻辑处理器P所共享的

  • mcentral对象收集所有给定规格大小的span

  • 每个mcentral都包含两个mspan链表:一个表示有空闲对象的mspan,一个表示没有空闲对象的span

  • 除了0级mspan,每个级别的span都会有一个mcentral用于管理span链表,所有的mcentral都是一个数组,由mheap进行管理

2.3 mheap

  • mheap的作用不只是管理mcentral,大对象页通过mheap进行分配。

  • 对mheap的操作必须全局加锁,由此mcache、mcentral可以看作某种形式的缓存

3 四级内存块管理

根据对象的大小,golang将内存返程了如图所示的HeapArea、chunk、与page四种内存块进行管理

  • HeapArea内存块最大,在unix64占64MB

  • chunk 占512KB

  • span根据级别的大小的不同而不同,但必须是page的倍数

  • 一个page占8KB

不同的内存块用于不同的场景,便于高效的对内存进行管理

4 对象分配

内存分配时,将对象划分为微小对象小对象大对象

内存分配的主要逻辑在runtime/malloc.go878行mallocgc函数
函数的主要逻辑如下:

// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	if size <= maxSmallSize {
        if noscan && size < maxTinySize {
            // Tiny allocator.
            // 微小对象分配
        } else {
           // 小对象
        }
    } else {
        // 大对象
    }
}

4.1 微小对象

go将小于16字节的对象划分为微小对象。划分微小对象的主要目的是处理极小的字符串和独立的转义变量。

微小对象会被放到2级span中(2级span的大小为16字节)

  • 小对象分配源码
            // 1. 首先,微小对象会按照2、4、8字节对齐
            off := c.tinyoffset
            if size&7 == 0 {
                off = alignUp(off, 8)
            } else if goarch.PtrSize == 4 && size == 12 {
                off = alignUp(off, 8)
            } else if size&3 == 0 {
                off = alignUp(off, 4)
            } else if size&1 == 0 {
                off = alignUp(off, 2)
            }
            // 查看当前分配的元素中是否有剩余的空间
            if off+size <= maxTinySize && c.tiny != 0 {
                x = unsafe.Pointer(c.tiny + off)
                // 分配完成后offset的位置也需要增加,为下次分配做准备
                c.tinyoffset = off + size
                c.tinyAllocs++
                mp.mallocing = 0
                releasem(mp)
                return x
            }
            // Allocate a new maxTinySize block.
            span = c.alloc[tinySpanClass]
            // 如果当前span的空间不够,将尝试从mcache中查找span中的快速下一个可用的元素
            v := nextFreeFast(span)
            if v == 0 {
                // 如果在mcache没有,则尝试去mcentral中查找
                v, span, shouldhelpgc = c.nextFree(tinySpanClass)
            }
            x = unsafe.Pointer(v)
            (*[2]uint64)(x)[0] = 0
            (*[2]uint64)(x)[1] = 0
            // See if we need to replace the existing tiny block with the new one
            // based on amount of remaining free space.
            if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
                // Note: disabled when race detector is on, see comment near end of this function.
                c.tiny = uintptr(x)
                c.tinyoffset = size
            }
            size = maxTinySize
4.1.1 mcache缓存位图

查找空闲元素空间时,首先要从mcache中找到对应的mspan,mspan中有allocCache字段其作为一个位

图,用于标记span元素是否被分配

  • nextFreeFast函数源码,重要内容我都写道注释里,方便与代码结合
// nextFreeFast returns the next free object if one is quickly available.
// Otherwise it returns 0.
func nextFreeFast(s *mspan) gclinkptr {
	// allocCache 是小端序,右边最后一位代表span第一个元素
	// TrailingZeros64函数代表s.allocCache从右边开始第一个0的位置
	theBit := sys.TrailingZeros64(s.allocCache) // Is there a free object in the allocCache?
	// 还要注意一个十分重要的问题:
	// 在runtime/mheap.go的1370行的initSpan函数中有关于allocCache 字段的初始化问题
	// 在1398~1400行有关于freeindex和allocCache初始化的代码,拷贝如下
	//     s.freeindex = 0
	//     s.allocCache = ^uint64(0) // all 1s indicating all free.
	// 可以看到allocCache字段初始化后,所有的比特位都是1
	// 再结合下面几行代码:s.allocCache >>= uint(theBit + 1)可以看出:
	// 如果theBit < 64代表则表示allocCache所有位置都是0,
	if theBit < 64 {
		// 当theBit的末尾一直是`...111111`的时候,theBit总是0
		// 确保小于freeindex的都已经被分配
		result := s.freeindex + uintptr(theBit)
		if result < s.nelems {
			// 找到可分配的位置
			freeidx := result + 1
			// 如果这个位置是64的倍数且freeidx != s.nelems
			if freeidx%64 == 0 && freeidx != s.nelems {
				return 0
			}
			s.allocCache >>= uint(theBit + 1)
			s.freeindex = freeidx
			s.allocCount++
			return gclinkptr(result*s.elemsize + s.base())
		}
	}
	return 0
}
4.1.2 mcentral遍历span

当前的span中没有可用的元素,这是就需要从mcentral中加锁寻找。

查找时会遍历mcentral中的有空闲元素的链表和没有空闲元素的链表,去查找有没有合适的span
之所以还会去遍历没有空闲元素的链表,是因为有些span虽然被标记为空闲,但是还没来得及清理,这
些span在清扫后仍然可以使用。

如果在mcentral中查找有空闲的span,将其赋值到,并更新allocCache,同时需要将span添加mcentral
的empty链表中去。

4.1.3 mheap缓存查找

go1.14之后每个逻辑处理器都维护了一个page cache

type pageCache struct {
	base uintptr
	cache uint64
	scav uint64
}

mheap会首先查找,每个逻辑处理器P中的page cache字段,cache字段页代表一个位图
每一位都代表一个page(8KB),由于cache为uint64,因此一共可以提供512KB的连续虚拟内存。

当需要分配的内存小于512/4=128KB时需要首先从cache中分配。

当分配的page过大或者在逻辑处理器P的cache没有找到可用的page就需要对mheap加锁在一颗基数树中查找

4.1.4 操作系统内存申请

当基数树中不到相应的内存时,需要从操作系统获取内存,在UNIX系统中最终使用mmap系统调用操作系统申请内存,

每次像操作系统申请内存的大小必须为heapArea的倍数。
heapArea是和平台有关的内存大小。其大小为64MB。

注意:这里申请的内存为虚拟内存,只有实际写入的空间为程序实际占用的大小

4.2 小对象

当对象不属于小对象时,在内存分配时会继续判断是否为小对象,小对象指小于32KB的对象。

分配空间时会计算小对象对应哪一个等级的span,并在指定的span中查找

此后的操作和微小对象分配一样,小对象的分配经理mcache、mcentral、mheap位图、mheap基数树、操作系统分配的过程

4.3 大对象

大对象指大于32KB的对象,内存分配时直接通过mheap进行分配。大对象的分配经历:mheap基数树查找、操作系统分配的过程,每个大对象都时一个特殊的span,级别为0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值