页缓存 — runtime.pageCache
在每个P
中都有一个pageCache
类型的字段pcache
,我们称为页的缓存。就是它保存了一组等待被使用的连续的页。对于要分配的的page
数小于16
个page
的内存,我们都会先从pcache
中查找是否有空闲的空间可供分配,pageCache
结构定义如下:
// go 1.20.3 path: /src/runtime/runtime2.go
type p struct {
......
pcache pageCache
......
}
// go 1.20.3 path: /src/runtime/mpagecache.go
// pageCache 用于存储和管理内存页的缓存信息。
type pageCache struct {
base uintptr // base 字段是一个无符号整型指针,通常用于存储内存页缓存的基地址。
cache uint64 // cache 字段是一个无符号64位整数,可能用于标识或跟踪缓存中的内存页状态,比如哪些页是空闲的,哪些是已分配的。
scav uint64 // scav 字段也是一个无符号64位整数,可能用于记录被清理(scavenged)的内存页信息。这通常涉及到回收和重用内存页的策略。
}
pageCache
结构体被用于实现 P
(处理器)的本地内存页缓存,这种结构体的设计有助于提高内存分配的效率和性能,特别是在多线程或并发环境下。
结合pageCache
的结构定义,base
表示这一组连续页的起始地址,cache
是一个uint64
对象,在64
位机器上是8
个字节,即64
个bit
。cache
中每个bit
表示一个page
是否是空闲的还是被分配出去了,1
表示空闲,0
表示已分配出去了。scav
表示已清除页面的64
位位图。每个页面大小为8KB
, 所以pcache
管理的这片内存大小为64*8K=512KB
。
如下图:
再来看看pageCache
的相关操作函数:
runtime.pageCache.empty — 判断pageCache是否为空
pageCache.empty
方法用来快速检查 pageCache
实例中是否包含任何缓存的内存页。在内存管理的上下文中,如果 pageCache
是空的,可能意味着当前没有可用的内存页面缓存,系统可能需要从其他地方获取页面或执行某些回收策略。
源码如下:
// go 1.20.3 path: /src/runtime/mpagecache.go
// empty 方法用于检查 pageCache 是否为空,返回一个布尔值,表示 pageCache 中的 cache 字段是否为 0。
func (c *pageCache) empty() bool {
// 如果 c.cache 等于 0,表示没有缓存的页面,即 pageCache 是空的,返回 true。
// 否则,表示 pageCache 中有缓存的页面,返回 false。
return c.cache == 0
}
runtime.pageCache.alloc — 从pageCache分配内存
runtime.pageCache.alloc
函数是 pageCache
结构体的一个关键方法,用于从局部页面缓存中分配内存,这可以提高内存分配的效率,尤其是在并发环境下。通过这种方式,Go
运行时尝试最大限度地减少对全局内存分配器的依赖,从而降低锁的竞争和提高性能。
runtime.pageCache.alloc
源码如下:
// go 1.20.3 path: /src/runtime/mpagecache.go
// alloc 方法用于从 pageCache 中分配指定数量的页面。
func (c *pageCache) alloc(npages uintptr) (uintptr, uintptr) {
// 如果 pageCache 的 cache 字段为 0,表示没有可用的页面,因此返回两个 0。
if c.cache == 0 {
return 0, 0
}
// 如果请求分配的页面数为 1。
if npages == 1 {
// 找到 cache 中第一个可用页面的索引。
i := uintptr(sys.TrailingZeros64(c.cache))
// 检查该页面是否被标记为已清理(scavenged)。
scav := (c.scav >> i) & 1
// 更新 cache,标记该页面已被分配。
c.cache &^= 1 << i
// 更新 scav,标记该页面已不再是已清理状态。
c.scav &^= 1 << i
// 计算并返回分配的页面的地址和该页面是否是已清理页面的标记。
return c.base + i*pageSize, uintptr(scav) * pageSize
}
// 如果请求分配的页面数大于 1,则调用 allocN 方法处理。
return c.allocN(npages)
}
这个方法的主要流程如下:
-
首先检查缓存是否为空(
c.cache == 0
)。如果为空,则直接返回两个零值,表示没有可分配的页面; -
如果请求分配单个页面(
npages == 1
),方法会找到第一个可用的页面,更新页面缓存和清理(scavenged
)状态,然后返回该页面的地址和清理状态;那如何找到第一个可用的页面呢?其实就是从
c.cache
中找到一个bit
为1
的位置,该位置对应的page
是未分配出去的,这里有一个算法,就是从cache
右边向左边找(理解为从低位向高位),对应的函数就是sys.TrailingZeros64
,该函数接收1
个uint64
的整数,实现的功能是从最低位到第一个非零位之前的零的个数。通俗来说,就是对于一个数x
, 它对应的二进制为ax
,然后从右往左统计ax
中0
的个数,遇到1
则结束统计。举个栗子说明:
假设输入数字是
8
,其二进制表示为1000
。从右边数,最低的三位是零,所以sys.TrailingZeros64(8)
将返回3
。接着再来说下
&^
的位运算,该符号做的操作就是将运算符左边数据相异的位保留,相同位清零。执行
c.cache &^= 1 << i
:表示将c.cache
中的第i
位清零,即将i
位标记为分配状态;执行
c.scav &^ = 1 << i
:表示将c.scav
中的第i
位清零,即将i
位标记为未清理状态;下面用一张图来表示经过
alloc
操作前后的结构和数据状态变化: -
对于请求多个页面的情况,方法会调用另一个函数
allocN
来处理更复杂的分配逻辑。allocN
的源码如下:// go 1.20.3 path: /src/runtime/mpagecache.go // allocN 方法从 pageCache 中分配指定数量(npages)的连续页面。 func (c *pageCache) allocN(npages uintptr) (uintptr, uintptr) { // 在 c.cache 中找到足够大的连续空闲位的起始索引。 i := findBitRange64(c.cache, uint(npages)) // 如果找不到足够的连续空间(返回值 >= 64),则返回两个零值。 if i >= 64 { return 0, 0 } // 构造一个掩码,用于选中从索引 i 开始的 npages 个位。 mask := ((uint64(1) << npages) - 1) << i // 计算被清理(scavenged)的页面数量。 scav := sys.OnesCount64(c.scav & mask) // 更新 cache 和 scav 字段,清除已分配的页面位。 c.cache &^= mask c.scav &^= mask // 计算分配的页面的基地址。 // 返回基地址和被清理页面的总大小。 return c.base + uintptr(i*pageSize), uintptr(scav) * pageSize }
我们总结下该函数的基本流程:
-
使用
findBitRange64
函数在pageCache
的cache
字段中查找足够大的连续空闲位区域,这个区域需要有足够的空间来容纳请求的页面数npages
,如果找到的起始索引i
大于或等于64
,则返回两个零值,表示分配失败;findBitRange64
函数作用是在一个 64 位无符号整数(uint64
)中查找一段连续的、足够大的空闲位区域,该区域至少包含n
个连续的0
。以下是对这个函数的逐行解释:// go 1.20.3 path: /src/runtime/mpallocbits.go // findBitRange64 在 64 位无符号整数 c 中查找至少包含 n 个连续 0 的区域 func findBitRange64(c uint64, n uint) uint { p := n - 1 // 计算所需连续0的数量减 1 k := uint(1) // 初始化 k,用于控制右移的位数 // 循环,直到找到足够的连续 0 或者遍历完整个数字 for p > 0 { // 如果剩余所需的 0 的数量小于或等于 k,进行最后一次比较 if p <= k { c &= c >> (p & 63) // 将 c 与其自身右移 p 位的结果进行按位与操作 break // 跳出循环 } // 否则,将 c 与其自身右移 k 位的结果进行按位与操作 c &= c >> (k & 63) // 如果 c 变为 0,表示没有找到足够的连续 0 if c == 0 { return 64 // 返回 64,表示未找到 } // 减少剩余所需的 0 的数量,将 k 倍增 p -= k k *= 2 } // 使用 sys.TrailingZeros64 计算 c 中最低位的连续 0 的数量,并返回该值 return uint(sys.TrailingZeros64(c)) }
-
构造一个掩码
mask
,用于在cache
中标记要分配的页面。这个掩码对应于从索引i
开始的npages
个连续位;掩码
mask := ((uint64(1) << npages) - 1) << i
的这个位运算非常巧妙,例如:npage
为3
,i
为2
,则mask
则为11100
;npage
为5
,i
为3
,则mask
则为11111000
;可以看出
1
的位置始终跟着i
位置变化,mask
值始终跟c.cache
和c.scav
分配位置异或值为1
。 -
使用位与操作 (
&
) 将mask
应用于scav
字段,然后用sys.OnesCount64
计算结果中的1
的数量。这表示被清理(scavenged
)的页面数量; -
使用位清除操作 (
&^=
) 更新cache
和scav
字段,清除已经分配的页面位。这表示这些页面现在已被占用,不再可用于后续分配; -
计算并返回分配的页面地址和被清理页面的大小。
-
runtime.pageAlloc.allocToCache — 通过pageAlloc分配pageCache
如果申请的内存比较大或者线程的页缓存中内存不足,会通过runtime.pageAlloc.alloc
从页堆分配一块页面缓存(pageCache
),函数代码如下:
// go 1.20.3 path: /src/runtime/mpagecache.go
// allocToCache 从 pageAlloc 分配一个页面缓存
func (p *pageAlloc) allocToCache() pageCache {
// 确保持有 mheap 锁。
assertLockHeld(p.mheapLock)
// 如果搜索地址超出了 pageAlloc 的结束地址,返回一个空的 pageCache
if chunkIndex(p.searchAddr.addr()) >= p.end {
return pageCache{}
}
// 初始化一个新的 pageCache
c := pageCache{}
// 计算当前搜索地址的 chunk 索引
ci := chunkIndex(p.searchAddr.addr())
var chunk *pallocData
// 检查 summary 数据是否指示当前 chunk 有可用页面
if p.summary[len(p.summary)-1][ci] != 0 {
// 获取当前 chunk 的 pallocData。
chunk = p.chunkOf(ci)
// 在 chunk 中找到一个可用的页面。
j, _ := chunk.find(1, chunkPageIndex(p.searchAddr.addr()))
if j == ^uint(0) {
throw("bad summary data") // 如果找不到,抛出异常
}
// 设置 pageCache 的基地址和缓存
c = pageCache{
base: chunkBase(ci) + alignDown(uintptr(j), 64)*pageSize,
cache: ^chunk.pages64(j),
scav: chunk.scavenged.block64(j),
}
} else {
// 如果当前 chunk 没有可用页面,尝试在整个 pageAlloc 中找到一个可用页面
addr, _ := p.find(1)
if addr == 0 {
p.searchAddr = maxSearchAddr()
return pageCache{} // 如果找不到,返回一个空的 pageCache
}
ci := chunkIndex(addr)
chunk = p.chunkOf(ci)
// 设置 pageCache 的基地址和缓存
c = pageCache{
base: alignDown(addr, 64*pageSize),
cache: ^chunk.pages64(chunkPageIndex(addr)),
scav: chunk.scavenged.block64(chunkPageIndex(addr)),
}
}
// 分配页面并更新 chunk 的状态
cpi := chunkPageIndex(c.base)
chunk.allocPages64(cpi, c.cache)
chunk.scavenged.clearBlock64(cpi, c.cache & c.scav)
// 更新 pageAlloc 的状态
p.update(c.base, pageCachePages, false, true)
p.searchAddr = offAddr{c.base + pageSize*(pageCachePages-1)}
// 返回新分配的 pageCache
return c
}
函数 allocToCache
的主要流程可以总结如下:
- 验证锁状态:确保在调用此函数时持有
mheapLock
,这是为了线程安全。检查搜索地址的有效性:检查当前搜索地址是否已超出pageAlloc
结构体管理的内存范围。如果是,返回一个空的pageCache
。 - 初始化
pageCache
结构体:创建一个新的pageCache
实例。 - 在当前 chunk 中查找可用页面:
- 根据当前搜索地址计算所在的内存块(
chunk
)的索引。 - 如果在
summary
数据中找到指示当前chunk
有可用页面的标记,则尝试在该chunk
中找到一个可用的页面。
- 根据当前搜索地址计算所在的内存块(
- 处理找到可用页面的情况:
- 如果找到可用页面,则设置
pageCache
的基地址、缓存和被清理页面的状态。 - 更新
chunk
的状态,标记相应的页面已被分配。
- 如果找到可用页面,则设置
- 处理没有找到可用页面的情况:
- 如果当前
chunk
没有可用页面,尝试在整个pageAlloc
中找到一个可用页面。 - 更新
pageCache
的基地址、缓存和被清理页面的状态。
- 如果当前
- 更新
pageAlloc
的状态:标记已分配的页面,更新searchAddr
以便下一次分配。返回配置好的pageCache
实例:返回设置好的pageCache
实例,用于后续的页面分配操作。
初始化
在 Go
语言中,内存的划分和初始化主要发生在程序启动时的运行时系统初始化过程中。这个过程涉及到多个函数和组件的协作。关键的函数和步骤包括:
runtime.mallocinit — 初始化内存分配系统
mallocinit
的主要职责是配置和初始化内存分配系统,确保在程序开始执行之前,内存分配环境已经准备妥当。
mallocinit
函数核心源码如下:
// go 1.20.3 path: /src/runtime/malloc.go
func mallocinit() {
// ......
// 初始化全局内存分配器 mheap。
mheap_.init()
// 为第一个处理器分配并初始化 mcache,用于本地小对象的快速分配。
mcache0 = allocmcache()
// 初始化一系列锁,这些锁用于不同的内存管理相关操作。
lockInit(&gcBitsArenas.lock, lockRankGcBitsArenas)
lockInit(&profInsertLock, lockRankProfInsert)
lockInit(&profBlockLock, lockRankProfBlock)
lockInit(&profMemActiveLock, lockRankProfMemActive)
for i := range profMemFutureLock {
lockInit(&profMemFutureLock[i], lockRankProfMemFuture)
}
lockInit(&globalAlloc.mutex, lockRankGlobalAlloc)
// 以下部分基于不同的架构和操作系统设置内存分配的初始地址。
if goarch.PtrSize == 8 {
// 对于 64 位系统,根据不同的平台和配置设置内存分配的起始地址。
for i := 0x7f; i >= 0; i-- {
var p uintptr
// 根据不同的构建配置和操作系统,选择不同的内存地址范围。
// 这是为了避免与其他应用程序或系统分配的内存冲突。
switch {
case raceenabled:
// 当启用数据竞争检测时的地址计算。
p = uintptr(i)<<32 | uintptrMask&(0x00c0<<32)
if p >= uintptrMask&0x00e000000000 {
continue
}
// 对于特定架构和操作系统,设置特定的内存地址。
case GOARCH == "arm64" && GOOS == "ios":
p = uintptr(i)<<40 | uintptrMask&(0x0013<<28)
case GOARCH == "arm64":
p = uintptr(i)<<40 | uintptrMask&(0x0040<<32)
case GOOS == "aix":
if i == 0 {
continue
}
p = uintptr(i)<<40 | uintptrMask&(0xa0<<52)
default:
p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
}
// 根据竞争检测状态和地址范围,选择合适的 hintList。
hintList := &mheap_.arenaHints
if (!raceenabled && i > 0x3f) || (raceenabled && i > 0x5f) {
hintList = &mheap_.userArena.arenaHints
}
// 分配一个新的 arenaHint,并将其加入到 hintList。
hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
hint.addr = p
hint.next, *hintList = *hintList, hint
}
} else {
// 对于 32 位系统,执行相应的内存地址设置。
// ......
}
}
在这个函数中,我们可以看到几个关键点:
- 内存分配器的初始化:
mheap_.init()
和allocmcache()
是初始化内存管理系统的关键步骤。它们分别初始化全局内存分配器和为第一个处理器分配内存缓存; - 锁的初始化:多个
lockInit
调用初始化了内存管理中使用的各种锁,这对于确保在多线程环境中内存操作的安全性至关重要; - 内存分配地址的设置:根据不同的架构和操作系统,函数设置了一系列不同的内存分配起始地址。这是为了避免与系统或其他程序的内存分配冲突;
- arenaHint 的配置:这部分代码基于计算出的地址创建并配置
arenaHint
,这对于优化内存分配效率非常重要。
runtime.mheap.init — 堆的初始化
在 mallocinit
调用之后,mheap.init
函数被触发,它进一步初始化 mheap
结构体,确保它在程序运行期间正确地分配和管理内存。
runtime.mheap.init
源码如下:
// go 1.20.3 path: /src/runtime/mheap.go
func (h *mheap) init() {
// 初始化 mheap 的两个锁,分别用于管理 heap 的基本操作和特殊场景。
lockInit(&h.lock, lockRankMheap) // 用于基本的 heap 操作。
lockInit(&h.speciallock, lockRankMheapSpecial) // 用于特殊场景,如最终处理器。
// 初始化 span 分配器。mspan 对象代表内存中的一段连续空间。
h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
// 初始化 mcache 分配器。mcache 为每个处理器(P)提供本地缓存,优化内存分配。
h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
// 初始化各种特殊目的的内存分配器。
h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
h.specialReachableAlloc.init(unsafe.Sizeof(specialReachable{}), nil, nil, &memstats.other_sys)
h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)
// 标记 span 分配器不应该返回已清零的内存。
h.spanalloc.zero = false
// 初始化中央缓存(central)。这里每个 central 管理一类大小的内存块。
for i := range h.central {
h.central[i].mcentral.init(spanClass(i))
}
// 初始化页表,管理内存页的分配和回收。
h.pages.init(&h.lock, &memstats.gcMiscSys)
}
其主要流程如下:
-
锁的初始化:
- 初始化
mheap
结构中的锁。这些锁用于在多线程环境中保护对堆的访问,防止并发问题。
- 初始化
-
span 分配器的初始化、缓存分配器的初始化:
- 初始化用于分配
mspan
对象的分配器。mspan
对象代表内存中的一段连续空间,是堆内存管理的基本单元。 - 初始化用于
mcache
对象的分配器。mcache
是每个处理器(P
)的本地内存缓存,用于加快小对象的分配。
在
heap
中,spanalloc
、cachealloc
、specialfinalizeralloc
、specialprofilealloc
、arenaHintAlloc
等都是fixalloc
分配器类型,所以其初始化都是对其fixalloc
分配器的初始化。在Go内存管理(上)的文章中已经对fixalloc
分配器的初始化、分配、回收操作进行过分析,此处不重复了。 - 初始化用于分配
-
特殊分配器的初始化以及arena 相关设置:
- 初始化一系列特殊用途的内存分配器,例如用于特殊内存块的分配器(如带有终结器的对象、性能分析数据等)。
- 根据系统架构和运行时的需求,设置内存分配的初始位置和策略。
-
中央缓存的初始化:
- 初始化
mcentral
结构,这是mheap
的一部分,负责管理特定大小的内存块。每个大小类都有一个对应的mcentral
。
- 初始化
-
页表的初始化:
- 初始化页表结构,这是管理和跟踪分配的内存页的机制。
runtime.mcentral.init — mcentral的初始化
mcentral
的初始化非常简单,如下代码:
// go 1.20.3 path: /src/runtime/mcentral.go
func (c *mcentral) init(spc spanClass) {
// 设置 mcentral 管理的 span 类别。
c.spanclass = spc
// 初始化 partial 列表的锁。partial 列表存储部分占用的 span。
lockInit(&c.partial[0].spineLock, lockRankSpanSetSpine)
lockInit(&c.partial[1].spineLock, lockRankSpanSetSpine)
// 初始化 full 列表的锁。full 列表存储完全占用的 span。
lockInit(&c.full[0].spineLock, lockRankSpanSetSpine)
lockInit(&c.full[1].spineLock, lockRankSpanSetSpine)
}
通过这个初始化过程,mcentral
被配置为管理一类特定大小的内存块,并通过锁机制保证并发环境下内存分配和回收的安全性。 mcentral
在自己初始化之后不会立即分配 span
,而是根据程序运行时的实际内存分配需求动态地进行。
具体来说,mcentral
分配 span
的过程通常遵循以下步骤:
- 内存分配请求:
- 当程序执行过程中需要分配内存时(如通过
new
或make
函数),Go
运行时会根据请求的大小确定对应的大小类,并找到相应的mcentral
。
- 当程序执行过程中需要分配内存时(如通过
- 检查空闲
span
:mcentral
首先会检查其维护的空闲span
列表。如果有可用的空闲span
,它将直接从这些span
中分配内存。
- 请求新的
span
:- 如果
mcentral
没有可用的空闲span
,它需要从全局内存分配器mheap
请求一个新的span
。这通常是通过调用mcentral.grow
函数完成的。
- 如果
mheap
提供新的span
:- 在
mcentral.grow
内部,最终会调用mheap.alloc
函数来从全局堆中分配一个新的span
。 - 新分配的
span
随后被切分成小块,并加入到mcentral
的空闲列表中,以供后续的内存分配请求使用。
- 在
- 持续的内存管理:
- 在程序的后续执行过程中,每当有新的内存分配请求,且
mcentral
需要更多的内存时,这个过程会重复进行。
- 在程序的后续执行过程中,每当有新的内存分配请求,且
mcentral.grow
函数以及mheap.alloc
函数后续内存分配会涉及,这边暂时不具体展开了。
runtime.pageAlloc.init — pageAlloc分配器初始化
pageAlloc
是一个专门用于管理和分配内存页(pages
)的分配器。它是 Go
运行时内存管理系统的一个关键部分,负责对内存页级别的分配和回收进行管理。通过这个 runtime.pageAlloc.init
方法,pageAlloc
被正确初始化,准备好进行内存页的管理和分配。
源码如下:
// go 1.20.3 path: /src/runtime/mpagealloc.go
func (p *pageAlloc) init(mheapLock *mutex, sysStat *sysMemStat) {
// 检查页级别日志大小是否超过最大允许值。
if levelLogPages[0] > logMaxPackedValue {
print("runtime: root level max pages = ", 1<<levelLogPages[0], "\n")
print("runtime: summary max pages = ", maxPackedValue, "\n")
throw("root level max pages doesn't fit in summary")
}
// 设置系统内存统计的引用。
p.sysStat = sysStat
// 初始化 inUse 位图,它标记了哪些页正在使用中。
p.inUse.init(sysStat)
// 执行系统级别的初始化,可能涉及到操作系统相关的内存设置。
p.sysInit()
// 设置搜索地址,这是分配内存时开始搜索的地址。
p.searchAddr = maxSearchAddr()
// 设置 mheap 锁的引用,这个锁用于同步对全局堆的访问。
p.mheapLock = mheapLock
}
runtime.allocmcache — mcache初始化
mcache
的初始化发生在两个地方:
-
P0
的初始化发生在mallocinit()
中,其调用为:mcache0 = allocmcache()
; -
其他
P
的初始化在func procresize(nprocs int32) *p
中,procresize
函数也在schedinit()
中调用,代码如下:// go 1.20.3 path: /src/runtime/proc.go func procresize(nprocs int32) *p { // 省略代码 ...... for i := old; i < nprocs; i++ { pp := allp[i] if pp == nil { pp = new(p) } pp.init(i) atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) } func (pp *p) init(id int32) { // 省略代码 ...... if pp.mcache == nil { if id == 0 { if mcache0 == nil { throw("missing mcache?") } pp.mcache = mcache0 } else { pp.mcache = allocmcache() } } // 省略代码 ...... }
不管哪里初始化,其实都是调用 allocmcache()
方法,其源码如下:
// go 1.20.3 path: /src/runtime/mcache.go
func allocmcache() *mcache {
var c *mcache
systemstack(func() {
// 锁定全局堆(mheap)的锁。
lock(&mheap_.lock)
// 从 mheap 的 cachealloc 分配器中分配一个新的 mcache。
c = (*mcache)(mheap_.cachealloc.alloc())
// 设置 mcache 的 flushGen 值为当前的 sweepgen,用于垃圾回收。
c.flushGen.Store(mheap_.sweepgen)
// 解锁全局堆的锁。
unlock(&mheap_.lock)
})
// 初始化 mcache 中的 alloc 数组,该数组用于小对象分配。
for i := range c.alloc {
c.alloc[i] = &emptymspan
}
// 设置 mcache 的 nextSample,这是用于控制内存分配采样的。
c.nextSample = nextSample()
// 返回新分配的 mcache。
return c
}
分析代码,其主要流程如下:
-
全局堆锁定:
- 函数首先在
systemstack
上下文中执行,这是出于安全和效率的考虑。lock(&mheap_.lock)
锁定全局堆(mheap
),确保分配操作的原子性和线程安全。
- 函数首先在
-
分配
mcache
:- 通过
mheap_.cachealloc.alloc()
从全局堆的缓存分配器中分配一个新的mcache
实例。
- 通过
-
设置
flushGen
:c.flushGen.Store(mheap_.sweepgen)
设置mcache
的flushGen
字段,这与垃圾回收机制有关,用于确保mcache
与当前的sweep
代保持同步。
-
初始化
alloc
数组:- 通过遍历
c.alloc
并设置每个元素为&emptymspan
,初始化mcache
中的内存分配数组。这个数组用于小对象的快速分配。
- 通过遍历
-
设置内存分配采样:
c.nextSample = nextSample()
设置内存分配采样的下一个阈值。这是一种性能监控机制,用于决定何时记录内存分配事件。
-
返回新分配的
mcache
:- 最后,函数返回新分配和初始化的
mcache
实例。
- 最后,函数返回新分配和初始化的
跟mcentral
一样,当一个 mcache
在 Go
语言运行时中初始化完毕后,它本身是空的,即它的各个大小类(span class
)的缓存中并没有预先分配的 span
。这意味着初始状态下的 mcache
不包含任何实际的内存块或 span
。当对应大小的内存分配请求首次发生时,mcache
会从会向对应的 mcentral
请求一个新的 span
。后续部分将在内存分配中具体分析。
至此,管理结构mheap
、67
个mcentral
及每个P
的mcache
都初始化完毕,接下来进入重点–分配阶段。
内存分配
一般我们写代码需要分配内存空间的时候,都用下列几种方式:
- new(T)
- &T{}
- make(xxxx)
但是无论是何种方式,何种写法,最终分配堆上内存都会通过调用 runtime.newobject
函数分配内存,该函数会调用 runtime.mallocgc
分配指定大小的内存空间,这也是用户程序向堆上申请内存空间的必经函数:
// go 1.20.3 path: /src/runtime/malloc.go
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
去除一些GC
、 race
以及debug
等一些非重点代码,runtime.mallocgc
主要源码如下:
// go 1.20.3 path: /src/runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 检查当前垃圾回收阶段,如果是标记终止阶段,则抛出异常。
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
// 对于请求分配0字节的内存,返回一个特殊的地址。
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// 省略一些调试和追踪相关的代码...
// 如果开启了内存对齐,调整请求的内存大小。
userSize := size
if asanenabled {
size += computeRZlog(size)
}
// 省略一些调试和追踪相关的代码...
// 计算 GC 辅助信用额度。
assistG := deductAssistCredit(size)
// 获取当前的 M(操作系统线程)并检查是否处于分配中的状态。
mp := acquirem()
if mp.mallocing != 0 {
throw("malloc deadlock")
}
if mp.gsignal == getg() {
throw("malloc during signal")
}
mp.mallocing = 1
// 省略一些内部状态设置...
//获取当前 p 对应的 mcache
c := getMCache(mp)
if c == nil {
throw("mallocgc called without a P or outside bootstrapping")
}
// 根据请求的大小,决定使用哪种内存分配策略。
var span *mspan
var x unsafe.Pointer
noscan := typ == nil || typ.ptrdata == 0
delayedZeroing := false
if size <= maxSmallSize {
if noscan && size < maxTinySize {
// 对于微对象的处理逻辑...
} else {
// 对于小对象的处理逻辑...
}
} else {
// 对于大对象的处理逻辑...
}
// 设置对象类型的元数据,用于垃圾回收。
if !noscan {
var scanSize uintptr
heapBitsSetType(uintptr(x), size, dataSize, typ)
// 省略一些逻辑...
}
// 省略一些追踪和分析相关的代码...
// 如果分配了需要延迟清零的内存,则在这里进行清零。
if delayedZeroing {
if !noscan {
throw("delayed zeroing on data that may contain pointers")
}
memclrNoHeapPointersChunked(size, x)
}
// 省略一些调试和追踪相关的代码...
// 如果当前分配导致需要触发 GC,则启动 GC。
if shouldhelpgc {
if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
gcStart(t)
}
}
// 返回分配的内存地址。
return x
}
const (
_TinySize = 16
_TinySizeClass = int8(2)
_MaxSmallSize = 32768
maxTinySize = _TinySize
tinySizeClass = _TinySizeClass
maxSmallSize = _MaxSmallSize
)
通过runtime.mallocgc
的代码可以知道,runtime.mallocgc
在分配内存的时候,会按照对象的大小以及是否为noscan
型空间来选择不同的分配策略了分为3
档来进行分配(**maxSmallSize
是32KB
,maxTinySize
**等于16
):
- 微对象: 小于
16
字节,而且是noscan
类型的内存分配请求,会使用tiny allocator
; - 大对象:大于
32KB
的内存分配,包括noscan
和scannable
类型,都会采用大块内存分配器; - 小对象:大于等于
16B
且小于等于32KB
的noscan
类型;以及不大于32KB
的scannable
类型的分配请求,都会直接匹配预置的大小规格来分配;

我们会依次介绍运行时分配微对象、小对象和大对象的过程,梳理内存分配的核心执行流程。
大对象(Large)分配
分配对象的大小超过32KB
视为大对象分配,大对象的分配流程与小对象和tiny
对象的分配是不一样的,大对象的分配直接走heap
进行分配。这其实也很好理解,因为预置的内存规格最大才32KB
,所以会直接根据需要的页面数分配一个新的span
。
无论是大对象还是小对象的分配统一入口都是runtime.newobject
,runtime.newobject
内部直接调用了runtime.mallocgc
函数,runtime.mallocgc
函数根据分配对象的大小做不同的处理逻辑,下面的代码只保留了大对象分配的核心逻辑大对象的分配,来看下大对象的内存分配处理逻辑:
// go 1.20.3 path: /src/runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 省略代码...
c := getMCache(mp)
// 省略代码...
if size <= maxSmallSize {
if noscan && size < maxTinySize {
// 对于微对象的处理逻辑...
} else {
// 对于小对象的处理逻辑...
}
} else {
// 设置标志表示可能需要辅助进行垃圾收集
shouldhelpgc = true
// 调用 allocLarge 从 mcache 分配一个大对象
// noscan 表示是否该对象包含指针,以此决定是否需要扫描
span = c.allocLarge(size, noscan)
// 初始化 span 的一些内部字段,用于跟踪分配情况
span.freeindex = 1 // 设置下一个可用的空闲索引为 1
span.allocCount = 1 // 设置已分配对象计数为 1
// 将 size 更新为 span 实际管理的每个元素的大小
size = span.elemsize
// 获取分配的内存块的基地址
x = unsafe.Pointer(span.base())
// 如果需要将分配的内存清零,并且 span 标记了需要清零
if needzero && span.needzero != 0 {
if noscan {
// 如果对象不包含指针,设置延迟清零标志
delayedZeroing = true
} else {
// 如果对象包含指针,立即清零
memclrNoHeapPointers(x, size)
}
}
}
// 省略代码...
return x
}
可以从代码看出大对象所分配的 mspan
是直接通过 mcache.largeAlloc
进行分配的,我们进一步看看mcache.largeAlloc
函数。
runtime.mcache.largeAlloc — 大对象分配
runtime.mcache.largeAlloc
函数源码如下:
// go 1.20.3 path: /src/runtime/mcache.go
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
// 检查 size 加上一个页面大小后是否会溢出。
// 这是一种内存安全检查。
if size+_PageSize < size {
throw("out of memory")
}
// 计算需要多少页面来满足请求的大小。
npages := size >> _PageShift
if size&_PageMask != 0 {
npages++
}
// 扣除为了进行清扫(sweeping)而积累的信用额度。
deductSweepCredit(npages*_PageSize, npages)
// 创建一个 span 类,表示是否扫描(noscan)。
spc := makeSpanClass(0, noscan)
// 从堆(mheap_)中分配所需页面数的 span。
s := mheap_.alloc(npages, spc)
if s == nil {
throw("out of memory")
}
// 更新内存统计信息。
stats := memstats.heapStats.acquire()
atomic.Xadd64(&stats.largeAlloc, int64(npages*pageSize))
atomic.Xadd64(&stats.largeAllocCount, 1)
memstats.heapStats.release()
// 更新 GC 控制器的总分配计数。
gcController.totalAlloc.Add(int64(npages * pageSize))
// 更新 GC 控制器状态。
gcController.update(int64(s.npages*pageSize), 0)
// 将新分配的 span 推入中央分配器的 fullSwept 列表。
mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
// 设置 span 的 limit 字段为基地址加上请求的大小。
s.limit = s.base() + size
// 初始化 span 的堆位图。
s.initHeapBits(false)
// 返回新分配的 span。
return s
}
总结下该函数的主要流程如下:
- 检查内存溢出:首先检查请求的内存大小
size
加上一个页面大小_PageSize
后是否会造成整数溢出。如果会溢出,则抛出内存不足的异常; - 计算所需页面数:根据请求的内存大小
size
,计算所需的页面数npages
。这涉及到将size
右移页面位移_PageShift
,并根据页面掩码_PageMask
确定是否需要额外的页面来满足请求,例如size=32KB+1
,则需要分配(32KB+1)/8KB=5
个page
; - 扣除清扫信用:调用
deductSweepCredit
函数扣除为清扫(sweeping
)积累的信用额度,这是垃圾收集机制的一部分。这个这边简单说明下,Go
语言中规定申请一字节内存空间需要做多少扫描工作,这个值根据GC
扫描的进度更新计算的,而每次执行辅助GC
,最少要扫描64KB
,协程每次执行辅助GC
,多出来的部分会作为信用存储到当前G
中,就像信用卡的额度一样,后续再执行mallocgc()
时,只要信用额度用不完,就不用执行辅助GC
了,这块内容在GC
章节中细品; - 创建 span 类:使用
makeSpanClass
函数创建一个span
类(spc
),表示是否需要扫描(由noscan
参数决定),我们都知道,span
有68
种规格,其中0
号就是我们现在创建大对象所使用的; - 分配内存 span:从堆(
mheap_
)中调用alloc
方法分配所需页面数的span(s)
。如果内存不足,抛出内存不足的异常; - 剩余的一些更新和初始化操作,这些操作包括:
- 更新内存统计和GC 控制器:更新内存统计信息,包括大对象的分配总量和计数;向垃圾收集控制器(
gcController
)报告新分配的内存,更新总分配计数和控制器的状态; - 添加到中央分配器:将新分配的
span
添加到中央分配器(mheap_.central
)的fullSwept
列表中,以供后续使用; - 设置 span 限制:设置
span
的limit
字段,这是其管理的内存区域的上限; - 初始化堆位图:调用
s.initHeapBits
方法初始化span
的堆位图,这与垃圾收集相关;
- 更新内存统计和GC 控制器:更新内存统计信息,包括大对象的分配总量和计数;向垃圾收集控制器(
- 返回新分配的 span:返回新分配的
span
对象。
根据以上流程,我们可以得出大对象的分配流程图如下:
下面我们将对runtime.mcache.largeAlloc
函数中以及关联函数中出现过的重点函数进行分析。
runtime.mheap.alloc — 从堆中分配span
runtime.mheap.alloc
方法分配一个新的mspan
,真正分配是runtime.mheap.allocSpan
做的,它本身除了的逻辑不多,主要是在runtime.mheap.allocSpan
中进行,然后就是对不用的页面进行清扫回收,对分配到的内存根据是否需要清0
做一些清理操作,代码如下:
// go 1.20.3 path: /src/runtime/mheap.go
// alloc 是 mheap 类型的一个方法,用于分配内存。
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
var s *mspan // 定义一个指向 mspan 类型的指针 s。
// systemstack 是一个运行时函数,用于在系统栈上执行给定的函数。
// 这通常用于执行可能涉及运行时操作的函数,以避免死锁或堆栈溢出。
systemstack(func() {
// 检查是否完成了内存清理(垃圾回收的一部分)。
if !isSweepDone() {
// 如果清理尚未完成,则先执行内存回收。
h.reclaim(npages)
}
// 分配内存空间。allocSpan 是一个分配特定数量页面的函数,
// spanAllocHeap 是一个枚举值,可能与分配的类型或优先级有关。
s = h.allocSpan(npages, spanAllocHeap, spanclass)
})
return s // 返回分配的内存空间的指针。
}
我们进一步看看主要工作进行的runtime.mheap.allocSpan
的实现。
runtime.mheap.allocSpan — 从堆中分配span
runtime.mheap.allocSpan
函数负责在内存堆 (mheap
) 中分配内存页 (mspan
)。代码中包含了复杂的逻辑来处理物理页面对齐、内存页的查找与分配、内存增长、以及内存统计和回收。 该函数代码码对于理解 Go
语言中内存分配和垃圾回收机制的内部工作原理是非常重要的。
由于其源码较为复杂并冗长,我们将流程归纳总结,按着先后和功能拆分着来分析:
-
初始化和预处理
首先,
runtime.mheap.allocSpan
函数会进行初始化和预处理,主要的工作是:- 获取当前
goroutine
的g
对象; - 初始化一些本地变量,如基地址
base
、回收页面数scav
和内存增长量growth
; - 检查是否需要根据物理页面大小进行对齐(对于栈空间分配特别重要)。
这部分代码源码如下:
// go 1.20.3 path: /src/runtime/mheap.go func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { gp := getg() // 获取当前 goroutine 的 g 对象。 base, scav := uintptr(0), uintptr(0) // 初始化 base 和 scav 变量,用于存储内存页的基地址和回收的页面数。 growth := uintptr(0) // 用于记录增长的内存大小。 // 判断是否需要按物理页面对齐。 needPhysPageAlign := physPageAlignedStacks && typ == spanAllocStack && pageSize < physPageSize //省略后续逻辑代码 ...... }
- 获取当前
-
尝试从页面缓存中分配
该步骤主要的工作是:
- 检查当前的
P
(处理器)是否有足够的页面缓存可供分配; - 如果页面缓存为空,锁定
heap
并分配页面到缓存; - 从缓存中分配所需的页面,并尝试创建一个新的
mspan
(内存跨度)。
该部分源码如下:
// go 1.20.3 path: /src/runtime/mheap.go // allocSpan 是 mheap 类型的一个方法,用于分配指定数量的内存页。 func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { //省略前面流程代码,需要查看代码请往上翻看 ...... pp := gp.m.p.ptr() // 获取当前 P 的指针。 // 如果不需要物理页面对齐,且当前 P 存在,且请求的页面数小于页面缓存的四分之一。 if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 { c := &pp.pcache // 获取 P 的页面缓存。 // 如果页面缓存为空,则锁定 heap 并分配页面到缓存,然后解锁。 if c.empty() { lock(&h.lock) *c = h.pages.allocToCache() unlock(&h.lock) } // 从缓存中分配页面。 base, scav = c.alloc(npages) if base != 0 { s = h.tryAllocMSpan() // 尝试分配 mspan。 if s != nil { goto HaveSpan // 如果成功分配则跳转到 HaveSpan。 } } } //省略后续逻辑代码 ...... }
从上述代码可以看出,当满足不需要物理对齐且
npages
小于16
个就会尝试从P
的页面缓存(pp.pcache
)中获取可用内存。 - 检查当前的
-
物理页面对齐处理
该步骤主要流程是:
- 如果需要物理页面对齐,计算额外需要的页面数:
extraPages := physPageSize / pageSize
; - 调用
heap.pages.find
查找以获取足够的连续空间,如果没有获取到的情况下,尝试调用runtime.mheap.grow
去扩容heap
,然后再次heap.pages.find
查找; - 确保基地址按物理页面对齐。
该部分源码如下:
// go 1.20.3 path: /src/runtime/mheap.go func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { //省略前面流程代码,需要查看代码请往上翻看 ...... lock(&h.lock) // 锁定 heap。 // 如果需要物理页面对齐。 if needPhysPageAlign { // 计算额外需要的页面数。 extraPages := physPageSize / pageSize // 在 heap 中查找足够的空闲页面。 base, _ = h.pages.find(npages + extraPages) if base == 0 { var ok bool // 如果没有找到,则尝试增长 heap。 growth, ok = h.grow(npages + extraPages) if !ok { unlock(&h.lock) return nil // 如果增长失败,返回 nil。 } // 重新查找空闲页面。 base, _ = h.pages.find(npages + extraPages) if base == 0 { throw("grew heap, but no adequate free space found") // 如果仍然没有找到,抛出异常。 } } // 对齐基地址。 base = alignUp(base, physPageSize) // 分配页面范围。 scav = h.pages.allocRange(base, npages) } //省略后续逻辑代码 ...... }
- 如果需要物理页面对齐,计算额外需要的页面数:
-
直接从 heap 中分配
如果到这步,
base
仍然为0
,则在heap
中分配页面,主要工作如下:- 调用
heap.pages.alloc
分配所需的页面; - 如有必要,尝试调用
runtime.mheap.grow
去增长heap
以提供足够的空间,然后再次调用heap.pages.alloc
分配页面; - 如果尚未创建
mspan
,锁定heap
并创建一个新的mspan
。
该步骤代码如下:
// go 1.20.3 path: /src/runtime/mheap.go // allocSpan 是 mheap 类型的一个方法,用于分配指定数量的内存页。 func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { //省略前面流程代码,需要查看代码请往上翻看 ...... // 如果 base 仍然为 0,则在 heap 中分配页面。 if base == 0 { base, scav = h.pages.alloc(npages) if base == 0 { var ok bool // 如果没有分配到,则尝试增长 heap。 growth, ok = h.grow(npages) if !ok { unlock(&h.lock) return nil // 如果增长失败,返回 nil。 } // 再次尝试分配页面。 base, scav = h.pages.alloc(npages) if base == 0 { throw("grew heap, but no adequate free space found") // 如果仍然没有分配到,抛出异常。 } } } // 如果 s 仍然为 nil,则锁定状态下分配 mspan。 if s == nil { s = h.allocMSpanLocked() } unlock(&h.lock) // 解锁 heap。 //省略后续逻辑代码 ...... }
- 调用
-
初始化分配的 span、内存回收和统计、返回mspan
到了这步,就是一些初始化
mspan
与关于内存回收和统计的操作了,主要有:- 使用分配的基地址、页面数等信息初始化
mspan
; - 根据当前的内存使用情况和回收目标,决定是否执行内存回收;
- 更新各种内存统计数据;
- 返回新创建的或获取的
mspan
指针。
这部分代码如下:
// go 1.20.3 path: /src/runtime/mheap.go func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { //省略前面流程代码,需要查看代码请往上翻看 ...... HaveSpan: // 以下是一系列关于内存回收和统计的操作…… // 省略部分代码以便注释更简洁。 // 初始化分配的 span。 h.initSpan(s, typ, spanclass, base, npages) nbytes := npages * pageSize // 计算分配的字节总数。 // 如果有回收的页面,则标记为已使用,并更新相关统计信息。 if scav != 0 { sysUsed(unsafe.Pointer(base), nbytes, scav) gcController.heapReleased.add(-int64(scav)) } // 更新 heap 的空闲和使用统计。 gcController.heapFree.add(-int64(nbytes - scav)) if typ == spanAllocHeap { gcController.heapInUse.add(int64(nbytes)) } // 更新内存统计信息。 stats := memstats.heapStats.acquire() atomic.Xaddint64(&stats.committed, int64(scav)) atomic.Xaddint64(&stats.released, -int64(scav)) switch typ { case spanAllocHeap: atomic.Xaddint64(&stats.inHeap, int64(nbytes)) case spanAllocStack: atomic.Xaddint64(&stats.inStacks, int64(nbytes)) case spanAllocPtrScalarBits: atomic.Xaddint64(&stats.inPtrScalarBits, int64(nbytes)) case spanAllocWorkBuf: atomic.Xaddint64(&stats.inWorkBufs, int64(nbytes)) } memstats.heapStats.release() pageTraceAlloc(pp, now, base, npages) // 记录页面分配信息。 return s // 返回分配的 mspan。 }
- 使用分配的基地址、页面数等信息初始化
至此,整个runtime.mheap.allocSpan
函数分析完成。在该函数中,出现过的runtime.mheap.pages.alloc
、 runtime.mheap.pages.allocToCache
、 runtime.mheap.pages.find
等函数在前面章节已经分析过,再次不重复了,我们下面分析分析runtime.mheap.pages.grow
函数,看看该函数是如何进行heap
的增长操作的。
runtime.mheap.grow — 扩容堆空间
runtime.pageAlloc.alloc
在分配失败时的处理,如果alloc
分配失败,需要从操作系统新申请一片空间,然后继续走alloc
进行分配,申请新空间处理逻辑在下面的grow
方法中,我们来看看 grow
源码:
// go 1.20.3 path: /src/runtime/mheap.go
// grow 方法用于增加 mheap 的大小以容纳更多页面
func (h *mheap) grow(npage uintptr) (uintptr, bool) {
// 确保持有 mheap 的锁,这是为了线程安全
assertLockHeld(&h.lock)
// 计算请求的页面大小,并按 pallocChunkPages 对齐
ask := alignUp(npage, pallocChunkPages) * pageSize
// 初始化 totalGrowth 为 0,用于记录增长的总大小
totalGrowth := uintptr(0)
// 计算预期增长后的末地址
end := h.curArena.base + ask
// 确保新的基地址按物理页面大小对齐
nBase := alignUp(end, physPageSize)
// 检查是否需要从系统申请更多内存
if nBase > h.curArena.end || end < h.curArena.base {
// 从系统申请内存
av, asize := h.sysAlloc(ask, &h.arenaHints, true)
// 如果申请失败,打印错误并返回
if av == nil {
inUse := gcController.heapFree.load() + gcController.heapReleased.load() + gcController.heapInUse.load()
print("runtime: out of memory: cannot allocate ", ask, "-byte block (", inUse, " in use)\n")
return 0, false
}
// 如果新申请的内存紧邻当前 arena 的末尾,直接扩展
if uintptr(av) == h.curArena.end {
h.curArena.end = uintptr(av) + asize
} else {
// 否则,映射新的内存区域,并更新统计和状态
if size := h.curArena.end - h.curArena.base; size != 0 {
sysMap(unsafe.Pointer(h.curArena.base), size, &gcController.heapReleased)
stats := memstats.heapStats.acquire()
atomic.Xaddint64(&stats.released, int64(size))
memstats.heapStats.release()
h.pages.grow(h.curArena.base, size)
totalGrowth += size
}
h.curArena.base = uintptr(av)
h.curArena.end = uintptr(av) + asize
}
// 更新 nBase 为新的对齐地址
nBase = alignUp(h.curArena.base+ask, physPageSize)
}
// 设置新的基地址
v := h.curArena.base
h.curArena.base = nBase
// 映射新的内存区域
sysMap(unsafe.Pointer(v), nBase-v, &gcController.heapReleased)
// 更新统计信息
stats := memstats.heapStats.acquire()
atomic.Xaddint64(&stats.released, int64(nBase-v))
memstats.heapStats.release()
// 增长 pages 并更新 totalGrowth
h.pages.grow(v, nBase-v)
totalGrowth += nBase - v
// 返回增长的总大小和成功标志
return totalGrowth, true
}
下面还是同样梳理下流程:
-
计算请求的内存大小并准备增长内存:
- 确保在执行
grow
方法时持有mheap
的锁,以保证线程安全; - 根据请求的页面数
npage
计算出需要申请的内存大小ask
,并确保它按照pallocChunkPages
的倍数对齐; - 初始化用于记录增长的总大小的变量
totalGrowth
,计算增长后的末地址end
和按物理页面大小对齐的新基地址nBase
;
- 确保在执行
-
判断是否需要申请新内存以及系统内存申请:
- 如果新基地址
nBase
超过当前arena
的结束地址或end
小于当前arena
的基地址,表示需要向系统申请新内存; - 调用
runtime.mheap.sysAlloc
从操作系统中申请更多的内存; - 如果申请失败(返回
nil
),打印内存不足的错误信息,并返回false
表示申请失败;
为了缩减篇幅,我们简单的介绍下
runtime.mheap.sysAlloc
函数,其缩减版源码如下:// go 1.20.3 path: /src/runtime/malloc.go func (h *mheap) sysAlloc(n uintptr, hintList **arenaHint, register bool) (v unsafe.Pointer, size uintptr) { n = alignUp(n, heapArenaBytes) // 尝试从 arena 中分配内存。 if hintList == &h.arenaHints { v = h.arena.alloc(n, heapArenaBytes, &gcController.heapReleased) if v != nil { size = n return v, size } } // 如果 arena 中分配失败,遍历 hintList 尝试分配。 for *hintList != nil { hint := *hintList p := hint.addr if hint.down { p -= n } v = sysReserve(unsafe.Pointer(p), n) if p == uintptr(v) { hint.addr = p + n size = n return v, size } *hintList = hint.next } // 直接从系统中分配对齐的内存。 v, size = sysReserveAligned(nil, n, heapArenaBytes) if v == nil { return nil, 0 } // 更新 hintList。 // 省略更新 hintList 的代码。 return v, size }
在这个简化的版本中,我们保留了以下核心步骤,其主要流程如下:
- 尝试从
arena
分配内存; - 如果
arena
分配失败,遍历hintList
尝试在提供的地址上预留内存; - 如果遍历
hintList
仍然失败,直接从系统中分配对齐的内存; - 更新
hintList
。
- 如果新基地址
-
处理新申请的内存:
- 如果新申请的内存紧邻当前
arena
的末尾,直接扩展当前arena
; - 否则,处理当前
arena
的映射,并将新申请的内存设置为当前arena
。
- 如果新申请的内存紧邻当前
-
映射新内存区域、更新统计信息:
- 使用
sysMap
函数映射新的内存区域; - 更新与内存相关的统计信息,如已释放的内存量。
- 使用
-
增长内部页面管理:
调用
h.pages.grow
来增长内部页面管理结构,记录新的内存页面。 -
返回增长结果:
返回总增长的内存大小
totalGrowth
和成功标志true
。
到这里,基本大对象的分配介绍完成,我们更新下流程图,方便更全面的了解:
小对象(small)分配
小对象是指大小在[16B,32KB]
范围或者小于16B
但是包含指针的对象。小对象的分配也是走的mallocgc
函数:
// go 1.20.3 path: /src/runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 省略代码...
c := getMCache(mp)
// 省略代码...
if size <= maxSmallSize {
if noscan && size < maxTinySize {
// 对于微对象的处理逻辑...
} else {
// 定义变量 sizeclass,用于存储内存大小类别。
var sizeclass uint8
// 根据请求的内存大小确定内存大小类别。
if size <= smallSizeMax-8 {
// 对于较小的内存请求,使用 size_to_class8 查找表来确定大小类别。
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
// 对于较大的内存请求,使用 size_to_class128 查找表来确定大小类别。
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
// 根据内存大小类别获取实际分配的内存大小。
size = uintptr(class_to_size[sizeclass])
// 创建一个 span 类别,这里使用 noscan 类别(表示不需要垃圾回收扫描)。
spc := makeSpanClass(sizeclass, noscan)
// 从当前 mcache 的 alloc 数组中获取对应大小类别的 span。
span = c.alloc[spc]
// 尝试快速从 span 中获取一个可用的内存块。
v := nextFreeFast(span)
if v == 0 {
// 如果快速获取失败,使用 nextFree 方法来获取内存块,并检查是否需要协助垃圾回收。
v, span, shouldhelpgc = c.nextFree(spc)
}
// 将内存地址转换为 unsafe.Pointer 类型。
x = unsafe.Pointer(v)
// 如果需要零初始化内存,并且 span 表示需要零初始化,则执行零初始化。
if needzero && span.needzero != 0 {
memclrNoHeapPointers(x, size)
}
}
} else {
// 对于大对象的处理逻辑...
}
// 省略代码...
return x
}
从上面代码,概括起来概括起来小对象的分配分为三个步骤:
- 创建
spanclass
, 根据分配对象的大小,结合size_to_class8
或size_to_class128
表,得到对象的sizeclass
, 然后根据sizeclass
以及对象是否包含指针计算得到spanclass
; - 拿到计算得到的
spanclass
从p.mcache
保存的对应规格的span
,然后从span
对象中获取一个空闲的object
; - 如果步骤
2
获取失败,则会从mcentral
获取一个新的span
对象,加入到mcache
中,然后在从刚申请的span
中分配一个空的object
。
将流程转化为流程图:
下面对小对象分配流程中出现的一些函数进行分析讲解:
runtime.nextFreeFast — 快速从mcache获取空闲对象
nextFreeFast
快速从mspan
中找到一个空闲的object
,传入的mspan
来自mcache
, mcache
中有一个*mspan
数组alloc
,缓存了各个规格(span class
)的span
对象,每个span
管理的page
已按预定的size
切分好了。
其源码如下:
// go 1.20.3 path: /src/runtime/malloc.go
// nextFreeFast 从 mspan 快速分配一个内存块
func nextFreeFast(s *mspan) gclinkptr {
// 使用 sys.TrailingZeros64 查找 allocCache 中第一个可用位的位置
theBit := sys.TrailingZeros64(s.allocCache)
// 检查是否找到了可用的位
if theBit < 64 {
// 计算出下一个可用内存块的索引
result := s.freeindex + uintptr(theBit)
// 确保 result 在元素总数范围内
if result < s.nelems {
// 计算新的 freeindex
freeidx := result + 1
// 如果新的 freeindex 是 64 的倍数且不是最后一个元素,返回 0
if freeidx%64 == 0 && freeidx != s.nelems {
return 0
}
// 更新 allocCache,移除已分配的位
s.allocCache >>= uint(theBit + 1)
// 更新 freeindex 和 allocCount。
s.freeindex = freeidx
s.allocCount++
// 返回新分配内存块的地址
return gclinkptr(result*s.elemsize + s.base())
}
}
// 如果没有找到可用位或其他条件不满足,返回 0
return 0
}
nextFreeFast
函数是一个用于管理内存分配的例程。它的作用是在 mspan
结构(代表一块内存区域)中查找下一个可用的(未分配的)对象,并更新相关的分配状态。以下是这个函数的流程:
-
计算空闲对象的位索引:
- 使用
sys.TrailingZeros64(s.allocCache)
来找到allocCache
中第一个为零的位的位置。allocCache
是一个64
位的标记,其中的每一位表示mspan
中对应位置的对象是否已被分配(1
表示已分配,0
表示未分配)。sys.TrailingZeros64
返回的是从最低位到第一个为零的位的距离(位索引)。
- 使用
-
检查是否存在空闲对象:
- 如果
theBit
小于64
,说明在allocCache
中找到了空闲对象。
- 如果
-
计算并返回空闲对象的地址:
- 计算空闲对象的索引:
result := s.freeindex + uintptr(theBit)
。这里的result
是空闲对象在mspan
中的索引;检查result
是否小于mspan
中元素的总数 (s.nelems
)。如果是,说明找到了有效的空闲对象。
- 计算空闲对象的索引:
-
更新
mspan
的状态并返回分配的对象地址;如果没有找到空闲对象则返回0。
runtime.mcache.nextFree — 获取新的span
当nextFreeFast()
在mcache
线程缓存中的span
中没有寻找到空间, 则会调用nextFree()
函数,其是一个更通用的内存分配函数。它在 nextFreeFast
无法满足分配请求时被调用,用于处理更复杂的分配场景。
其源码如下:
// go 1.20.3 path: /src/runtime/malloc.go
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
//从mcache中获取当前 span 类型的缓存
s = c.alloc[spc]
//初始化 shouldhelpgc 为 false
shouldhelpgc = false
//调用 nextFreeIndex 方法来获取当前 span 中下一个空闲索引
freeIndex := s.nextFreeIndex()
//如果空闲索引等于 span 中元素的总数,则表示 span 已满
if freeIndex == s.nelems {
//如果在这种情况下 allocCount 不等于 nelems,则抛出异常,因为这意味着存在不一致
if uintptr(s.allocCount) != s.nelems {
println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount != s.nelems && freeIndex == s.nelems")
}
//尝试使用 refill 方法来获取新的 span
c.refill(spc)
//设置 shouldhelpgc 为 true,表明可能需要进行垃圾回收
shouldhelpgc = true
//重新获取新 span 的下一个空闲索引
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
//如果新的空闲索引无效(即大于或等于元素总数),则抛出异常
if freeIndex >= s.nelems {
throw("freeIndex is not valid")
}
//计算并返回内存块的实际地址
v = gclinkptr(freeIndex*s.elemsize + s.base())
//allocCount自增
s.allocCount++
//如果 allocCount 超出了元素总数,抛出异常
if uintptr(s.allocCount) > s.nelems {
println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
throw("s.allocCount > s.nelems")
}
//函数返回计算出的内存块地址 v,当前 span s,以及是否需要帮助垃圾回收的标志 shouldhelpgc
return
}
这个函数的目的是从 mcache
的当前 span
中找到下一个可用的内存块。当 mcache
的当前 span
没有足够的空间时,mcache.nextFree
会尝试从 mcentral
获取一个新的 span
,或者在必要时从 mheap
分配。
虽然比 nextFreeFast
慢,但 mcache.nextFree
提供了更完备的内存分配策略,并且可以处理 mcache
缓存不命中的情况。
runtime.mcache.refill — 从mcentral中获取空闲对象
当mcache
中的某种类型的span
已经没有可用空间时,则会调用runtime.mcache.refill
函数从mcentral
的列表获取一个新的所需大小规格的span
并替换已经不存在可用对象的结构体。
源码如下:
// go 1.20.3 path: /src/runtime/malloc.go
func (c *mcache) refill(spc spanClass) {
// 从当前 mcache 的 alloc 数组中获取指定 span 类型的当前 span
s := c.alloc[spc]
// 如果当前 span 的已分配对象计数不等于它所包含的元素总数,则抛出异常
if uintptr(s.allocCount) != s.nelems {
throw("refill of span with free space remaining")
}
// 如果当前 span 不是一个空的 span,则进行以下操作
if s != &emptymspan {
// 如果当前 span 的清扫代数不正确,则抛出异常
if s.sweepgen != mheap_.sweepgen+3 {
throw("bad sweepgen in refill")
}
// 将当前 span 从中心缓存移除
mheap_.central[spc].mcentral.uncacheSpan(s)
// 获取并更新内存统计信息
stats := memstats.heapStats.acquire()
slotsUsed := int64(s.allocCount) - int64(s.allocCountBeforeCache)
atomic.Xadd64(&stats.smallAllocCount[spc.sizeclass()], slotsUsed)
// 如果当前 span 类型是 tinySpanClass,更新 tinyAllocCount
if spc == tinySpanClass {
atomic.Xadd64(&stats.tinyAllocCount, int64(c.tinyAllocs))
c.tinyAllocs = 0
}
// 释放内存统计信息
memstats.heapStats.release()
// 更新总分配的内存字节数
bytesAllocated := slotsUsed * int64(s.elemsize)
gcController.totalAlloc.Add(bytesAllocated)
// 重置当前 span 的分配计数器
s.allocCountBeforeCache = 0
}
// 从中心缓存中获取一个新的 span
s = mheap_.central[spc].mcentral.cacheSpan()
// 如果获取不到新的 span(内存不足),抛出异常
if s == nil {
throw("out of memory")
}
// 检查新获取的 span 是否还有空闲空间,如果没有,则抛出异常
if uintptr(s.allocCount) == s.nelems {
throw("span has no free space")
}
// 设置新 span 的清扫代数
s.sweepgen = mheap_.sweepgen + 3
// 设置新 span 的分配前计数器
s.allocCountBeforeCache = s.allocCount
// 更新垃圾回收控制器的统计数据
usedBytes := uintptr(s.allocCount) * s.elemsize
gcController.update(int64(s.npages*pageSize)-int64(usedBytes), int64(c.scanAlloc))
c.scanAlloc = 0
// 更新 mcache 的 alloc 数组,将新的 span 放入其中
c.alloc[spc] = s
}
refill
函数的作用是在 mcache
的特定 span
类别耗尽时,从中心堆 (mheap_
) 中获取一个新的 span
并更新相关统计信息。其主要流程为:
- 获取当前 Span: 从
mcache
的alloc
数组中获取当前请求大小类 (spanClass
) 的span
;如果当前span
的已分配计数 (allocCount
) 与其元素总数 (nelems
) 不匹配,则抛出异常; - 处理非空 Span:
- 如果当前
span
不是空的,执行以下步骤:- 检查
span
的清扫代数 (sweepgen
) 是否正确,若不正确则抛出异常。 - 从中央缓存 (
mcentral
) 中移除当前span
。 - 更新内存统计信息。
- 对于特定大小类(如
tinySpanClass
),执行特定的统计更新。 - 计算并更新总分配的字节数。
- 重置
span
的分配前计数器 (allocCountBeforeCache
)。
- 检查
- 如果当前
- 从 mcentral 获取新 Span:
- 调用
mheap_.central[spc].mcentral.cacheSpan()
从中央缓存中获取一个新的span
。 - 如果无法获取新的
span
(例如,内存不足),抛出 “out of memory” 异常。 - 检查新获取的
span
是否有可用空间,如果没有则抛出异常。
- 调用
- 设置新 Span 的状态:
- 更新新
span
的清扫代数 (sweepgen
)、新span
的分配前计数器 (allocCountBeforeCache
)、垃圾回收控制器的统计数据。
- 更新新
- 将新 Span 放入 mcache:
- 更新
mcache
的alloc
数组,将新获取的span
放入其中。
- 更新
runtime.mcentral.uncacheSpan — 从mcentral 中移除span
uncacheSpan
函数从 mcentral
的缓存中移除一个 span
。它首先检查 span
是否有效(allocCount
不为 0
),然后根据 span
的清扫状态将其分类为过时或当前。对于过时的 span
,执行清扫操作;否则,根据 span
的剩余空间,将其添加到相应的清扫列表中。这个过程是内存管理的一部分,确保了有效的内存重用和垃圾回收。
源码如下:
// go 1.20.3 path: /src/runtime/mcentral.go
func (c *mcentral) uncacheSpan(s *mspan) {
// 如果尝试移除一个已分配计数为 0 的 span,则抛出异常
if s.allocCount == 0 {
throw("uncaching span but s.allocCount == 0")
}
// 获取当前的全局清扫代数
sg := mheap_.sweepgen
// 检查 span 是否过时(即它是否已经被清扫)
stale := s.sweepgen == sg+1
// 如果 span 是过时的,将它的清扫代数设置为 sg-1,否则设置为当前代数 sg
if stale {
atomic.Store(&s.sweepgen, sg-1)
} else {
atomic.Store(&s.sweepgen, sg)
}
// 如果 span 是过时的,进行清扫操作
if stale {
ss := sweepLocked{s}
ss.sweep(false)
} else {
// 如果 span 未满,将其添加到部分清扫列表中;如果已满,添加到完全清扫列表中
if int(s.nelems)-int(s.allocCount) > 0 {
c.partialSwept(sg).push(s)
} else {
c.fullSwept(sg).push(s)
}
}
}
runtime.mcentral.cacheSpan — 从mcentral中获取可用的span
runtime.mcentral.cacheSpan
函数作用是试图从不同的span
(内存块)集合中找到一个合适的span
进行分配。这涉及检查部分清扫、完全未清扫的span
,并在必要时增长内存池。
源码如下:
// go 1.20.3 path: /src/runtime/malloc.go
func (c *mcentral) cacheSpan() *mspan {
// 计算span的字节数
spanBytes := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) * _PageSize
deductSweepCredit(spanBytes, 0) // 可能用于调整GC的内存清扫信用
spanBudget := 100 // 用于控制循环的预算
var s *mspan
var sl sweepLocker
sg := mheap_.sweepgen
// 尝试从部分清扫的span中获取一个span
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}
// 开始清扫锁定过程
sl = sweep.active.begin()
if sl.valid {
// 尝试从部分未清扫的span中获取一个span
for ; spanBudget >= 0; spanBudget-- {
s = c.partialUnswept(sg).pop()
if s == nil {
break
}
// 如果获取成功,并且清扫成功,跳转到havespan
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true)
sweep.active.end(sl)
goto havespan
}
}
// 尝试从完全未清扫的span中获取一个span。
for ; spanBudget >= 0; spanBudget-- {
s = c.fullUnswept(sg).pop()
if s == nil {
break
}
// 如果获取成功,并且清扫成功,跳转到havespan。
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true)
freeIndex := s.nextFreeIndex()
// 检查是否有可用空间。
if freeIndex != s.nelems {
s.freeindex = freeIndex
sweep.active.end(sl)
goto havespan
}
// 如果没有可用空间,将其推回完全清扫的span中。
c.fullSwept(sg).push(s.mspan)
}
}
// 结束清扫过程。
sweep.active.end(sl)
}
// 尝试增长span。
s = c.grow()
if s == nil {
return nil
}
havespan:
// 检查span是否有可用对象。
n := int(s.nelems) - int(s.allocCount)
if n == 0 || s.freeindex == s.nelems || uintptr(s.allocCount) == s.nelems {
throw("span has no free objects")
}
// 计算空闲字节的基地址和位置。
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return s
}
该函数主要流程如下:
- 计算span的大小:
- 通过
class_to_allocnpages[c.spanclass.sizeclass()]
查找span
的大小,然后乘以页面大小(_PageSize
)来计算spanBytes
。
- 通过
- 扣除扫描信用:
deductSweepCredit(spanBytes, 0)
可能用于调整垃圾回收过程中的内存清扫信用。
- 初始化变量:
- 设置一个
spanBudget
,这是一个用于控制后续循环次数的预算。 - 声明
s
(指向mspan
的指针)和sl
(sweepLocker
类型)。
- 设置一个
- 从部分清扫的span中尝试获取span:
- 使用
c.partialSwept(sg).pop()
尝试获取一个已部分清扫的span
。 - 如果成功获取,跳转到
havespan
标签。
- 使用
- 开始清扫锁定过程:
- 通过
sweep.active.begin()
获取一个sweepLocker
。 - 如果
sl
是有效的,执行后续的循环。
- 通过
- 从部分未清扫的span中获取span:
- 在预算内循环,使用
c.partialUnswept(sg).pop()
尝试获取。 - 如果获取到
span
,并且能够成功清扫(sl.tryAcquire(s)
),则跳转到havespan
标签。
- 在预算内循环,使用
- 从完全未清扫的span中获取span:
- 类似的循环,但这次是从
c.fullUnswept(sg)
获取span
。 - 如果获取到
span
,并且能够成功清扫,并且还有可用空间,则更新s.freeindex
并跳转到havespan
标签。 - 如果没有可用空间,将
span
推回完全清扫的列表中。
- 类似的循环,但这次是从
- 结束清扫过程:
- 执行
sweep.active.end(sl)
来结束清扫过程。
- 执行
- 尝试增长span:
- 如果之前的步骤都未能获得
span
,则通过c.grow()
尝试增长span
。 - 如果仍然没有获得
span
,则返回nil
。
- 如果之前的步骤都未能获得
- 处理获取到的span并返回获取到的span。
runtime.mcentral.grow — 为 mcentral 结构分配一个新的 mspan
在runtime.mcentral.cacheSpan
函数中,如果在runtime.mcentral.partial
与runtime.mcentral.full
中都无法获取到相应的可用的span
,则会调用runtime.mcentral.grow
来扩容 mcentral
后再获取span
。
runtime.mcentral.grow
源码如下:
// go 1.20.3 path: /src/runtime/mcentral.go
func (c *mcentral) grow() *mspan {
// 计算根据当前大小类 (sizeclass) 需要多少页 (npages)。
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
// 根据大小类获取每个对象的大小。
size := uintptr(class_to_size[c.spanclass.sizeclass()])
// 从全局堆 (mheap) 分配所需数量的页来创建一个新的 span。
s := mheap_.alloc(npages, c.spanclass)
if s == nil {
// 如果分配失败,返回 nil。
return nil
}
// 计算可以在 span 中放入多少个该大小的对象。
n := s.divideByElemSize(npages << _PageShift)
// 设置 span 的限制地址。这是 span 中可以使用的内存的上限。
s.limit = s.base() + size*n
// 初始化 span 的堆位图,用于垃圾回收。
s.initHeapBits(false)
// 返回新分配的 span。
return s
}
通过 mcentral.grow
函数,Go
运行时能够动态地响应内存分配需求,为特定大小类别的对象分配适量的内存。这是确保内存分配既高效又灵活的关键机制之一。
至此,内存分配的小对象已经完成分析,更新下流程图,以便更直观的展示流程:
微对象(tiny)分配
微对象指分配的对象小于16B
,且不含指针。微对象采用tiny
分配器进行分配。tiny allocator
是 Go
运行时中一个针对小对象优化的内存分配器,专门用于处理小对象(通常是小于或等于 16
字节)的分配。
tiny allocator
它是通过每个 P
中的 mcache
实现的,其包含了与 tiny allocator
直接相关的字段(tiny
, tinyoffset
, tinyAllocs
)。当有微对象需要分配时,tiny allocator
会检查当前 tiny
块是否有足够的空间容纳这个对象。如果有,对象就直接在当前 tiny
块中分配,紧接着之前分配的对象。当当前 tiny
块没有足够空间容纳新的微对象时,tiny allocator
会从当前 P
的 mcache
中请求一个新的 span
,并从这个 span
中划分出一个新的 tiny
块用于后续的微对象分配。
在下面微对象分配的过程中,object
对象已分配10B
,还剩6B
,接下来申请一个3B
的内存空间,剩余的空间还够,继续从6B
中分出去3B
的空间,如下图所示:
当前的分配情况如上图所示,接下来申请一个8B
的内存空间,当前tiny
分配器中只剩下3B
空间,不够分配,则从mcache
中取一个16
空object
,从里面切出来8B
分配出去,还剩8B
保存在tiny
分配器中,供后续分配使用。如下图:
再来看看微对象的分配代码:
// go 1.20.3 path: /src/runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 省略代码...
c := getMCache(mp)
// 省略代码...
if size <= maxSmallSize {
if noscan && size < maxTinySize {
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)
}
// 检查当前 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 块没有足够的空间,从 mcache 分配一个新的 tiny 块。
span = c.alloc[tinySpanClass]
// 从span中找到一个空的object,这个object的大小为16byte
v := nextFreeFast(span)
if v == 0 {
// 尝试从mcentral中分配
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
// 清零新分配的内存。
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// 更新分配器中的字段
// 如果要分配的对象大小size比之前分配的要小即这次分配的object切分后剩下的空间比之前的要大,则保存这次剩下的空间
// 实际效果比较这次分配剩下的空间和前次分配剩下空间,那个大保留那个。
if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
}
} else {
// 对于小对象的处理逻辑...
} else {
// 对于大对象的处理逻辑...
}
// 省略代码...
return x
}
从代码可以看出,微对象的分配步骤基本跟小对象一致,无非在小对象分配流程前加了一步:检查当前 tiny
块是否有足够的空间来分配新对象,如果足够就在tiny
块中分配。
其流程图如下:
微对象的分配就不展开细说了,基本按着小对象走即可。
至此,内存分配就此讲完,等有时间了会再整理整理这个章节,现在内容有些混乱。