文章目录
- 计算机为什么需要内存?
- 为什么需要栈内存?
-
- 「栈内存」的简易管理过程:
-
- [1. 栈内存分配逻辑:current - alloc](#1. 栈内存分配逻辑:current - alloc)
- [2. 栈内存释放逻辑:current + release](#2. 栈内存释放逻辑:current + release)
- 为什么需要堆内存?
- Go语言分配的是虚拟内存
- Go语言栈内存的分配
- Go语言堆内存的分配
- 总结
- Go内存管理
- [1. TCMalloc](#1. TCMalloc)
-
- [1.1 Page](#1.1 Page)
- [1.2 Span](#1.2 Span)
- [1.3 ThreadCache](#1.3 ThreadCache)
- [1.4 CentralCache](#1.4 CentralCache)
- [1.5 PageHeap](#1.5 PageHeap)
- [1.6 TCMalloc对象分配](#1.6 TCMalloc对象分配)
- [2. Go内存管理](#2. Go内存管理)
-
- [2.1 Page](#2.1 Page)
- [2.2 Span](#2.2 Span)
- [2.3 mcache](#2.3 mcache)
- [2.4 mcentral](#2.4 mcentral)
- [2.5 mheap](#2.5 mheap)
- [2.6 内存分配](#2.6 内存分配)
- [3 垃圾回收](#3 垃圾回收)
-
- [3.1 标记-清除](#3.1 标记-清除)
- [3.2 三色可达性分析](#3.2 三色可达性分析)
- [3.3 gc触发](#3.3 gc触发)
- [3.4 gc过程](#3.4 gc过程)
计算机为什么需要内存?
计算机是运行自动化程序的载体,程序(或称之为进程)由可执行代码被执行后产生。那么计算机在运行程序的过程中为什么需要「内存」呢?为了轻松理解这个问题,我们先来简单看看:
- 代码的本质
- 可执行代码被执行后,程序的运行过程
代码的本质
简单来看代码主要包含两部分:
- 指令部分:中央处理器CPU可执行的指令
- 数据部分:常量等
代码包含了指令,代码被转化为可执行二进制文件,被执行后加载到内存中,中央处理器CPU通过内存获取指令,图示如下:

程序的运行过程
可执行代码文件被执行之后,代码中的待执行指令被加载到了内存当中。这时CPU就可以从内存中获取指令、并执行指令。
CPU执行指令简易过程分为三步:
- 取指:CPU控制单元从内存中获取指令
- 译指:CPU控制单元解析从内存中获取指令
- 执行:CPU运算单元负责执行具体的指令操作
我们通过一个简易的时序图来看看CPU获取并执行指令的过程:

内存的作用
通过以上我们可以基本看出「内存」在计算机中扮演的角色:
- 暂存二进制可执行代码文件中的指令、预置数据(常量)等
- 暂存指令执行过程中的中间数据
- 等等
至此我们基本明白了内存存在的意义。但是呢,我们又经常会听到关于「栈内存」、「堆内存」的概念,那「栈内存」和「堆内存」到底是什么呢?接下来我们继续来看看这个问题。
为什么需要栈内存?
程序在使用内存的过程中,不仅仅只需要关注内存的分配问题,还需要关注到内存使用完毕的回收问题,这就是内存管理中面临的最大两个问题:
- 内存的分配
- 内存的回收
有没有简单、高效、且通用的办法统一解决这个内存分配问题呢?
答:最简单、高效地分配和回收方式就是对一段连续内存的「线性分配」,「栈内存」的分配就采用了这种方式。
「栈内存」的简易管理过程:
1. 栈内存分配逻辑:current - alloc

2. 栈内存释放逻辑:current + release

通过利用「栈内存」,CPU在执行指令过程中可以高效的存储临时变量。其次:
- 栈内存的分配过程:看起来像不像数据结构「栈」的入栈过程。
- 栈内存的释放过程:看起来像不像数据结构「栈」的出栈过程。

所以同时你应该也理解了「为什么称之为栈内存?」。「栈内存」是计算机对连续内存的采取的「线性分配」管理方式,便于高效存储指令运行过程中的临时变量。
为什么需要堆内存?
假如函数A内变量是个指针且被函数B外的代码依赖,如果对应变量内存被回收,这个指针就成了野指针不安全。怎么解决这个问题呢?
答:这就是「堆内存」存在的意义,Go语言会在代码编译期间通过「逃逸分析」把分配在「栈」上的变量分配到「堆」上去。

「堆内存」如何回收呢?
答:堆内存通过「垃圾回收器」回收
Go语言分配的是虚拟内存
通过以上我们了解了「内存」、「栈内存」、「堆内存」存在的意义。除此之外,还有一个重要的知识点:程序和操作系统实际操作的都是虚拟内存,最终由CPU通过内存管理单元MMU(Memory Manage Unit)把虚拟内存的地址转化为实际的物理内存地址。图示如下:

使用虚拟内存的原因:
- 对于我们的进程而言,可使用的内存是连续的
- 安全,防止了进程直接对物理内存的操作(如果进程可以直接操作物理内存,那么存在某个进程篡改其他进程数据的可能)
- 提升物理内存的利用率,当进程真正要使用物理内存时再分配
- 虚拟内存和物理内存是通过MMU(管理单元内存Memory Management Unit)映射的
所以,一个很重要的知识点:
结论:Go语言源代码对「栈内存」和「堆内存」的分配、释放等操作,都是对虚拟内存的操作,最终中央处理器CPU会统一通过MMU(管理单元内存Memory Management Unit)转化为实际的物理内存。
也就是说Go语言源代码中:
- 「栈内存」的分配或释放都是对虚拟内存的操作
- 「堆内存」的分配或释放都是对虚拟内存的操作

接着我们分别通过分配时机、分配过程两部分,来看看Go语言栈内存和堆内存的分配。
Go语言栈内存的分配
Go语言栈内存分配的时机

栈内存分配时机-创建Goroutinue时
创建g0函数代码片段:
go
// src/runtime/proc.go::1720
// 创建 m
func allocm(_p_ *p, fn func(), id int64) *m {
// ...略
if iscgo || mStackIsSystemAllocated() {
mp.g0 = malg(-1)
} else {
// 创建g0 并申请8KB栈内存
// 依赖的malg函数
mp.g0 = malg(8192 * sys.StackGuardMultiplier)
}
// ...略
}
创建g函数代码片段:
go
// src/runtime/proc.go::3999
// 创建一个带有任务fn的goroutine
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
// ...略
newg := gfget(_p_)
if newg == nil {
// 全局队列、本地队列找不到g 则 创建一个全新的goroutine
// _StackMin = 2048
// 申请2KB栈内存
// 依赖的malg函数
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// ...略
}
以上都依赖malg函数代码片段,其作用是创建一个全新g:
go
// src/runtime/proc.go::3943
// 创建一个指定栈内存的g
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
// ...略
systemstack(func() {
// 分配栈内存
newg.stack = stackalloc(uint32(stacksize))
})
// ...略
}
return newg
}
栈内存分配时机-栈扩容
go
// src/runtime/stack.go::838
func copystack(gp *g, newsize uintptr) {
// ...略
// 分配新的栈空间
new := stackalloc(uint32(newsize))
// ...略
}
结论:创建Goroutine和栈扩容时,栈内存的分配都是由函数stackalloc分配。

所以,我们通过分析stackalloc函数就可以知道栈内存的分配过程了,具体如下。
栈内存分配过程
Go语言栈内存的分配按待分配的栈大小分为两大类:
- 小于32KB的栈内存
- 大于32KB的栈内存
小于32KB栈分配过程
- 先去M线程缓存mcache的栈内存缓存stackcache中分配:

- 如果stackcache内存不足,则从全局栈内存缓存池stackpool中分配:

- 如果stackpool内存不足,则从逻辑处理器结构p中的p.pagecache中分配:

- 如果p.pagecache内存不足,则从堆mheap中分配:

大于等于32KB栈分配过程
- 直接从全局栈内存缓存池stackLarge中分配:

- 全局栈内存缓存池stackLarge不足,则从逻辑处理器结构p中的p.pagecache中分配,如果p.pagecache则去堆上mheap分配:

Go语言堆内存的分配
Go语言堆内存分配时机
判断一个变量是否应该分配到「堆内存」的关键点就是:代码编译阶段,编译器会通过逃逸分析判断并标记上该变量是否需要分配到堆上。
通常我们在创建如下变量时,变量都有可能被分配到堆上:


切片分配过程源代码如下:
go
// 代码位置:src/cmd/compile/internal/gc/walk.go::1316
// 初始化切片
case OMAKESLICE:
// ...略...
// 逃逸标识,是否需要逃逸到堆上
if n.Esc == EscNone {
// ...略...
// 不需要逃逸
// 直接栈上分配内存
t = types.NewArray(t.Elem(), i) // [r]T
// ...略...
} else {
// 需要内存逃逸到堆上
// ...略...
// 默认使用makeslice64函数从堆上分配内存
fnname := "makeslice64"
argtype := types.Types[TINT64]
// ...略...
if (len.Type.IsKind(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
(cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
// 校验通过,则
// 使用makeslice函数从堆上分配内存
fnname = "makeslice"
argtype = types.Types[TINT]
}
// ...略...
// 调用上面指定的runtime函数
m.Left = mkcall1(fn, types.Types[TUNSAFEPTR], init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))
// ...略...
}
最终分配堆内存的地方都会依赖函数mallocgc,我们通过阅读mallocgc的代码就可以看到堆内存的分配过程。

Go语言堆内存分配过程
堆内存的分配按对象的大小分,主要分为三大类:
- 微对象 0 < Micro Object < 16B
- 小对象 16B =< Small Object <= 32KB
- 大对象 32KB < Large Object
「微对象」和「小对象」通常通过逻辑处理器结构P的线程缓存mcache分配,「大对象」直接从堆上mheap中分配,如下图所示:

线程缓存mcache的tiny结构主要负责分配「微对象」
线程缓存mcache的alloc结构主要负责分配「小对象」

微对象的分配过程
微对象 0 < Micro Object < 16B
- 线程缓存mcache的tiny内存充足,则直接分配「微对象」所需内存,图示如下:

- 线程缓存mcache的tiny内存不足,先去线程缓存mcache的alloc申请16B给tiny,再分配「微对象」所需内存,简易图示如下:

申请16B详细过程图示如下:

小对象的分配过程
小对象 16B =< Small Object <= 32KB
线程缓存mcache的alloc充足,则直接分配「小对象」所需内存,简易图示如下:

详细分配过程图示如下:

- 线程缓存mcache的alloc不足,则去中央缓存mcentral获取一个mspan,再分配「小对象」所需内存,图示如下:

- 线程缓存mcache的alloc不足,且中央缓存mcentral不足,则去逻辑处理器结构的p.pagecache分配,如果pagecache不足,直接去堆上mheap获取一个mspan,再分配「小对象」所需内存,图示如下:

大对象的分配过程
大对象 32KB < Large Object
- 逻辑处理器结构的pagecache充足,则直接分配「大对象」所需内存,图示如下:

- 逻辑处理器结构的pagecache不足,则直接去堆上mheap分配「大对象」所需内存,图示如下:

总结


Go内存管理
现代高级编程语言管理内存的方式分自动和手动两种。手动管理内存的典型代表是C和C++,编写代码过程中需要主动申请或者释放内存;而Java 和Go等语言使用自动的内存管理系统,由内存分配器和垃圾收集器来代为分配和回收内存,开发者只需关注业务代码而无需关注底层内存分配和回收,虽然语言帮我们处理了这部分但是还是有必要去了解一下底层的架构设计和执行逻辑,这样可以更好的掌握一门语言,本文主要以go内存管理为切入点再到go垃圾回收,系统的讲解一下go自动内存管理系统的设计和原理,由于篇幅有限略去了go垃圾回收三色标记屏障技术这一块,有兴趣的推荐去看下《go语言设计和实现》。
1. TCMalloc
go内存管理是借鉴了TCMalloc的设计思想,TCMalloc全称Thead-Caching Malloc,是google开发的内存分配器,为了方便理解下面的go内存管理,有必要要先熟悉一下TCMalloc。

1.1 Page
操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。
1.2 Span
一组连续的Page被称为Span ,比如可以有4个页大小的Span,也可以有8个页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
1.3 ThreadCache
每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
1.4 CentralCache
是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同 ,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。
1.5 PageHeap
PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时候,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap
1.6 TCMalloc对象分配
小对象直接从ThreadCache分配,若ThreadCache不够则从CentralCache中获取内存,CentralCache内存不够时会再从PageHeap获取内存,大对象在PageHeap中选择合适的页组成span用于存储数据。
2. Go内存管理
经过上一节对TCMalloc内存管理的描述,对接下来理解go的内存管理会有大致架构的熟悉,go内存管理架构取之TCMalloc不过在细节上有些出入,先来看一张go内存管理的架构图

2.1 Page
和TCMalloc中page相同,上图中最下方浅蓝色长方形代表一个page
2.2 Span
与TCMalloc中的Span相同,Span是go内存管理的基本单位,代码中为 mspan,一组连续的Page组成1个Span,所以,上图一组连续的浅蓝色长方形代表的是一组Page组成的1个Span,另外,1个淡紫色长方形为1个Span。
2.3 mcache
mcache与TCMalloc中的ThreadCache类似,mcache保存的是各种大小的Span ,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问 。但mcache与ThreadCache也有不同点,TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcache,因为在Go程序中,当前最多有GOMAXPROCS个线程在运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问,下图是G,P,M三者之间的关系

2.4 mcentral
mcentral与TCMalloc中的CentralCache类似,是所有线程共享的缓存,需要加锁访问,它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。但mcentral与CentralCache也有不同点,CentralCache是每个级别的Span有1个链表,mcache是每个级别的Span有2个链表。 【如果这里的级别指的是spanClass 那么就是一种级别有两种,scan类(包含指针的)和noscan类(不包含指针的)】
2.5 mheap
mheap与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS(系统)申请出的内存页组成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请,向OS的内存申请是按页来的,然后把申请来的内存页生成Span组织起来,同样也是需要加锁访问的 。但mheap与PageHeap也有不同点:mheap把Span组织成了树结构,而不是链表,并且还是2棵树,然后把Span分配到heapArena进行管理,它包含地址映射和span是否包含指针等位图,这样做的主要原因是为了更高效的利用内存:分配、回收和再利用。【实际是建立基数树索引来快速查找空闲内存的】
2.6 内存分配
Go中的内存分类并不像TCMalloc那样分成小、中、大对象,但是它的小对象里又细分了一个Tiny对象,Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象 。小对象和大对象只用大小划定,无其他区分,其中小对象大小在16Byte到32KB之间,大对象大小大于32KB 。span规格分类【这就是简单的划分一个span可以有多少个bytes,同时一个object占多少bytes】 上面说到go的内存管理基本单位是span,且span有不同的规格,要想区分出不同的span,我们必须要有一个标识,每个span通过spanclass标识属于哪种规格的span,golang的span规格一共有67种,具体如下:
/usr/local/go/src/internal/runtime/gc/sizeclasses.go
go
// class bytes/obj bytes/span objects tail waste max waste min align
// 1 8 8192 1024 0 87.50% 8
// 2 16 8192 512 0 43.75% 16
// 3 24 8192 341 8 29.24% 8
// 4 32 8192 256 0 21.88% 32
// 5 48 8192 170 32 31.52% 16
// 6 64 8192 128 0 23.44% 64
// 7 80 8192 102 32 19.07% 16
// 8 96 8192 85 32 15.95% 32
// 9 112 8192 73 16 13.56% 16
// 10 128 8192 64 0 11.72% 128
// 11 144 8192 56 128 11.82% 16
// 12 160 8192 51 32 9.73% 32
// 13 176 8192 46 96 9.59% 16
// 14 192 8192 42 128 9.25% 64
// 15 208 8192 39 80 8.12% 16
// 16 224 8192 36 128 8.15% 32
// 17 240 8192 34 32 6.62% 16
// 18 256 8192 32 0 5.86% 256
// 19 288 8192 28 128 12.16% 32
// 20 320 8192 25 192 11.80% 64
// 21 352 8192 23 96 9.88% 32
// 22 384 8192 21 128 9.51% 128
// 23 416 8192 19 288 10.71% 32
// 24 448 8192 18 128 8.37% 64
// 25 480 8192 17 32 6.82% 32
// 26 512 8192 16 0 6.05% 512
// 27 576 8192 14 128 12.33% 64
// 28 640 8192 12 512 15.48% 128
// 29 704 8192 11 448 13.93% 64
// 30 768 8192 10 512 13.94% 256
// 31 896 8192 9 128 15.52% 128
// 32 1024 8192 8 0 12.40% 1024
// 33 1152 8192 7 128 12.41% 128
// 34 1280 8192 6 512 15.55% 256
// 35 1408 16384 11 896 14.00% 128
// 36 1536 8192 5 512 14.00% 512
// 37 1792 16384 9 256 15.57% 256
// 38 2048 8192 4 0 12.45% 2048
// 39 2304 16384 7 256 12.46% 256
// 40 2688 8192 3 128 15.59% 128
// 41 3072 24576 8 0 12.47% 1024
// 42 3200 16384 5 384 6.22% 128
// 43 3456 24576 7 384 8.83% 128
// 44 4096 8192 2 0 15.60% 4096
// 45 4864 24576 5 256 16.65% 256
// 46 5376 16384 3 256 10.92% 256
// 47 6144 24576 4 0 12.48% 2048
// 48 6528 32768 5 128 6.23% 128
// 49 6784 40960 6 256 4.36% 128
// 50 6912 49152 7 768 3.37% 256
// 51 8192 8192 1 0 15.61% 8192
// 52 9472 57344 6 512 14.28% 256
// 53 9728 49152 5 512 3.64% 512
// 54 10240 40960 4 0 4.99% 2048
// 55 10880 32768 3 128 6.24% 128
// 56 12288 24576 2 0 11.45% 4096
// 57 13568 40960 3 256 9.99% 256
// 58 14336 57344 4 0 5.35% 2048
// 59 16384 16384 1 0 12.49% 8192
// 60 18432 73728 4 0 11.11% 2048
// 61 19072 57344 3 128 3.57% 128
// 62 20480 40960 2 0 6.87% 4096
// 63 21760 65536 3 256 6.25% 256
// 64 24576 24576 1 0 11.45% 8192
// 65 27264 81920 3 128 10.00% 128
// 66 28672 57344 2 0 4.91% 4096
// 67 32768 32768 1 0 12.50% 8192
// alignment bits min obj size
// 8 3 8
// 16 4 32
// 32 5 256
// 64 6 512
// 128 7 768
// 4096 12 28672
// 8192 13 32768

由上表可见最大的对象是32KB大小 ,超过32KB大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。所以上面只有列出了1-66。 内存大小转换 下面还要三个数组,分别是:class_to_size ,size_to_class 和 class_to_allocnpages3个数组,对应下图上的3个箭头:

以第一列为例,类别1的对象大小是8bytes,所以 class_to_size[1]=8 ;span大小是8KB,为1页,所以 class_to_allocnpages[1]=1,下为go源码中大小转换数组。
/usr/local/go/src/internal/runtime/gc/sizeclasses.go
go
// size class to obj size (byte)
var SizeClassToSize = [NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
// siez class to pages (page)
var SizeClassToNPages = [NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
// obj size to size class
var SizeClassToDivMagic = [NumSizeClasses]uint32{0, ^uint32(0)/8 + 1, ^uint32(0)/16 + 1, ^uint32(0)/24 + 1, ^uint32(0)/32 + 1, ^uint32(0)/48 + 1, ^uint32(0)/64 + 1, ^uint32(0)/80 + 1, ^uint32(0)/96 + 1, ^uint32(0)/112 + 1, ^uint32(0)/128 + 1, ^uint32(0)/144 + 1, ^uint32(0)/160 + 1, ^uint32(0)/176 + 1, ^uint32(0)/192 + 1, ^uint32(0)/208 + 1, ^uint32(0)/224 + 1, ^uint32(0)/240 + 1, ^uint32(0)/256 + 1, ^uint32(0)/288 + 1, ^uint32(0)/320 + 1, ^uint32(0)/352 + 1, ^uint32(0)/384 + 1, ^uint32(0)/416 + 1, ^uint32(0)/448 + 1, ^uint32(0)/480 + 1, ^uint32(0)/512 + 1, ^uint32(0)/576 + 1, ^uint32(0)/640 + 1, ^uint32(0)/704 + 1, ^uint32(0)/768 + 1, ^uint32(0)/896 + 1, ^uint32(0)/1024 + 1, ^uint32(0)/1152 + 1, ^uint32(0)/1280 + 1, ^uint32(0)/1408 + 1, ^uint32(0)/1536 + 1, ^uint32(0)/1792 + 1, ^uint32(0)/2048 + 1, ^uint32(0)/2304 + 1, ^uint32(0)/2688 + 1, ^uint32(0)/3072 + 1, ^uint32(0)/3200 + 1, ^uint32(0)/3456 + 1, ^uint32(0)/4096 + 1, ^uint32(0)/4864 + 1, ^uint32(0)/5376 + 1, ^uint32(0)/6144 + 1, ^uint32(0)/6528 + 1, ^uint32(0)/6784 + 1, ^uint32(0)/6912 + 1, ^uint32(0)/8192 + 1, ^uint32(0)/9472 + 1, ^uint32(0)/9728 + 1, ^uint32(0)/10240 + 1, ^uint32(0)/10880 + 1, ^uint32(0)/12288 + 1, ^uint32(0)/13568 + 1, ^uint32(0)/14336 + 1, ^uint32(0)/16384 + 1, ^uint32(0)/18432 + 1, ^uint32(0)/19072 + 1, ^uint32(0)/20480 + 1, ^uint32(0)/21760 + 1, ^uint32(0)/24576 + 1, ^uint32(0)/27264 + 1, ^uint32(0)/28672 + 1, ^uint32(0)/32768 + 1}
var SizeToSizeClass8 = [SmallSizeMax/SmallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
var SizeToSizeClass128 = [(MaxSmallSize-SmallSizeMax)/LargeSizeDiv + 1]uint8{32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67}
为对象寻找span 寻找span的流程如下:
-
计算对象所需内存大小size
-
根据size到size class映射,计算出所需的size class
-
根据size class和对象是否包含指针计算出span class
-
获取该span class指向的span
以分配一个包含指针大小为20Byte的对象为例,根据映射表:
go
// class bytes/obj bytes/span objects tail waste max waste min align
// 1 8 8192 1024 0 87.50% 8
// 2 16 8192 512 0 43.75% 16
// 3 24 8192 341 8 29.24% 8
size class 3,它的对象大小范围是(16,32]Byte,20Byte刚好在此区间,所以此对象的size class为3,Size class到span class的计算如下:
/usr/local/go/src/runtime/mheap.go
go
// noscan为false代表对象包含指针
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

所以,对应的span class为:
go
span class = 3 << 1 | 0 = 6
所以该对象需要的是span class 6指向的span,自此,小对象内存分配完成。
/usr/local/go/src/runtime/malloc.go
go
var sizeclass uint8
// step1: 确定规格sizeclass
if size <= gc.SmallSizeMax-8 {
sizeclass = gc.SizeToSizeClass8[divRoundUp(size, gc.SmallSizeDiv)]
} else {
sizeclass = gc.SizeToSizeClass128[divRoundUp(size-gc.SmallSizeMax, gc.LargeSizeDiv)]
}
size = uintptr(gc.SizeClassToSize[sizeclass])
// size class到span class
spc := makeSpanClass(sizeclass, false)
//step2: 分配对应spanclass 的 span
span := c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, checkGCTrigger = c.nextFree(spc)
}
x := unsafe.Pointer(v)
if span.needzero != 0 {
memclrNoHeapPointers(x, size)
}
大对象(>32KB)的分配则简单多了,直接在 mheap 上进行分配,首先计算出需要的内存页数和span class级别,然后优先从 free 中搜索可用的span,如果没有找到,会从 scav 中搜索可用的span,如果还没有找到,则向OS申请内存,再重新搜索2棵树,必然能找到span。如果找到的span比需求的span大,则把span进行分割成2个span,其中1个刚好是需求大小,把剩下的span再加入到 free 中去。
3 垃圾回收
3.1 标记-清除
标记-清除算法是第一种自动内存管理,基于追踪的垃圾收集算法。算法思想在 70 年代就提出了,是一种非常古老的算法。内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定哪些单元可以回收。算法分为两个部分:标记(mark)和清除(sweep)。标记阶段表明所有的存活单元,清扫阶段将垃圾单元回收。可视化可以参考下图。

标记-清除算法的优点就是基于追踪的垃圾回收算法具有的优点:避免了引用计数算法的缺点(不能处理循环引用,需要维护指针)【标记-清除通过"从根出发的可达性分析"处理循环引用:不可达的整圈都会在 sweep 时一起被回收】。缺点也很明显,需要STW。
3.2 三色可达性分析
三色标记算法是对标记阶段的改进,原理如下:
-
起初所有对象都是白色。
-
从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
-
从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
-
重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收

三色标记的一个明显好处是能够让用户程序和 mark 并发的进行 ,不过三色标记清除算法本身是不可以并发或者增量执行的,它需要STW,而如果并发执行,用户程序可能在标记执行的过程中修改对象的指针,导致可能将本该死亡的对象标记为存活和本该存活的对象标记为死亡,为了解决这种问题,go v1.8之后使用混合写屏障技术支持并发和增量执行,将垃圾收集的时间缩短至0.5ms以内。
3.3 gc触发
在堆上分配大于 32K byte 对象的时候进行检测此时是否满足垃圾回收条件,如果满足则进行垃圾回收
go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
shouldhelpgc := false
// 分配对象 > 32K byte
if size <= maxSmallSize {
...
} else {
shouldhelpgc = true
...
}
...
// gcShouldStart() 函数进行触发条件检测
if shouldhelpgc && gcShouldStart(false) {
// gcStart() 函数进行垃圾回收
gcStart(gcBackgroundMode, false)
}
}
// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
gcStart(gcForceBlockMode, false)
}
上面是自动垃圾回收,还有一种主动垃圾回收,通过调用 runtime.GC(),这是阻塞式的。
go
// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
gcStart(gcForceBlockMode, false)
}
系统gc触发条件:触发条件主要关注下面代码中的中间部分: forceTrigger || memstats.heap_live >= memstats.gc_trigger,forceTrigger 是 forceGC 的标志,后面半句的意思是当前堆上的活跃对象大于我们初始化的时候设置的 GC 触发阈值,在 malloc 以及 free 的时候 heap_live 会一直进行更新
go
// gcShouldStart returns true if the exit condition for the GCoff
// phase has been met. The exit condition should be tested when
// allocating.
//
// If forceTrigger is true, it ignores the current heap size, but
// checks all other conditions. In general this should be false.
func gcShouldStart(forceTrigger bool) bool {
return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
}
//初始化的时候设置 GC 的触发阈值
func gcinit() {
_ = setGCPercent(readgogc())
memstats.gc_trigger = heapminimum
...
}
// 启动的时候通过 GOGC 传递百分比 x
// 触发阈值等于 x * defaultHeapMinimum (defaultHeapMinimum 默认是 4M)
func readgogc() int32 {
p := gogetenv("GOGC")
if p == "off" {
return -1
}
if n, ok := atoi32(p); ok {
return n
}
return 100
}
3.4 gc过程
下列源码是基于 go 1.8,由于源码过长,所以这里只尽量只关注主流程
- gcStart
go
// gcStart 是 GC 的入口函数,根据 gcMode 做处理。
// 1. gcMode == gcBackgroundMode (后台运行,也就是并行), _GCoff -> _GCmark
// 2. 否则 GCoff -> _GCmarktermination, 这个时候是主动 GC
func gcStart(mode gcMode, forceTrigger bool) {
...
//在后台启动 mark worker
if mode == gcBackgroundMode {
gcBgMarkStartWorkers()
}
...
// Stop The World
systemstack(stopTheWorldWithSema)
...
if mode == gcBackgroundMode {
// GC 开始前的准备工作
//处理设置 GCPhase, setGCPhase 还会 开始写屏障
setGCPhase(_GCmark)
gcBgMarkPrepare() // Must happen before assist enable.
gcMarkRootPrepare()
// Mark all active tinyalloc blocks. Since we're
// allocating from these, they need to be black like
// other allocations. The alternative is to blacken
// the tiny block on every allocation from it, which
// would slow down the tiny allocator.
gcMarkTinyAllocs()
// Start The World
systemstack(startTheWorldWithSema)
} else {
...
}
}
- Mark
go
func gcStart(mode gcMode, forceTrigger bool) {
...
//在后台启动 mark worker
if mode == gcBackgroundMode {
gcBgMarkStartWorkers()
}
}
func gcBgMarkStartWorkers() {
// Background marking is performed by per-P G's. Ensure that
// each P has a background GC G.
for _, p := range &allp {
if p == nil || p.status == _Pdead {
break
}
if p.gcBgMarkWorker == 0 {
go gcBgMarkWorker(p)
notetsleepg(&work.bgMarkReady, -1)
noteclear(&work.bgMarkReady)
}
}
}
// gcBgMarkWorker 是一直在后台运行的,大部分时候是休眠状态,通过 gcController 来调度
func gcBgMarkWorker(_p_ *p) {
for {
// 将当前 goroutine 休眠,直到满足某些条件
gopark(...)
...
// mark 过程
systemstack(func() {
// Mark our goroutine preemptible so its stack
// can be scanned. This lets two mark workers
// scan each other (otherwise, they would
// deadlock). We must not modify anything on
// the G stack. However, stack shrinking is
// disabled for mark workers, so it is safe to
// read from the G stack.
casgstatus(gp, _Grunning, _Gwaiting)
switch _p_.gcMarkWorkerMode {
default:
throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
case gcMarkWorkerDedicatedMode:
gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
case gcMarkWorkerFractionalMode:
gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
case gcMarkWorkerIdleMode:
gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
}
casgstatus(gp, _Gwaiting, _Grunning)
})
}
}
Mark 阶段的标记代码主要在函数 gcDrain() 中实现
go
// gcDrain scans roots and objects in work buffers, blackening grey
// objects until all roots and work buffers have been drained.
func gcDrain(gcw *gcWork, flags gcDrainFlags) {
...
// Drain root marking jobs.
if work.markrootNext < work.markrootJobs {
for !(preemptible && gp.preempt) {
job := atomic.Xadd(&work.markrootNext, +1) - 1
if job >= work.markrootJobs {
break
}
markroot(gcw, job)
if idle && pollWork() {
goto done
}
}
}
// 处理 heap 标记
// Drain heap marking jobs.
for !(preemptible && gp.preempt) {
...
//从灰色队列中取出对象
var b uintptr
if blocking {
b = gcw.get()
} else {
b = gcw.tryGetFast()
if b == 0 {
b = gcw.tryGet()
}
}
if b == 0 {
// work barrier reached or tryGet failed.
break
}
//扫描灰色对象的引用对象,标记为灰色,入灰色队列
scanobject(b, gcw)
}
}
- Sweep
go
func gcSweep(mode gcMode) {
...
//阻塞式
if !_ConcurrentSweep || mode == gcForceBlockMode {
// special case synchronous sweep.
...
// Sweep all spans eagerly.
for sweepone() != ^uintptr(0) {
sweep.npausesweep++
}
// Do an additional mProf_GC, because all 'free' events are now real as well.
mProf_GC()
mProf_GC()
return
}
// 并行式
// Background sweep.
lock(&sweep.lock)
if sweep.parked {
sweep.parked = false
ready(sweep.g, 0, true)
}
unlock(&sweep.lock)
}
对于并行式清扫,在 GC 初始化的时候就会启动 bgsweep(),然后在后台一直循环
go
func bgsweep(c chan int) {
sweep.g = getg()
lock(&sweep.lock)
sweep.parked = true
c <- 1
goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
for {
for gosweepone() != ^uintptr(0) {
sweep.nbgsweep++
Gosched()
}
lock(&sweep.lock)
if !gosweepdone() {
// This can happen if a GC runs between
// gosweepone returning ^0 above
// and the lock being acquired.
unlock(&sweep.lock)
continue
}
sweep.parked = true
goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
}
}
func gosweepone() uintptr {
var ret uintptr
systemstack(func() {
ret = sweepone()
})
return ret
}
不管是阻塞式还是并行式,最终都会调用 sweepone()。上面说过 go 内存管理都是基于 span 的,mheap_ 是一个全局的变量,所有分配的对象都会记录在 mheap_ 中。在标记的时候,我们只要找到对象对应的 span 进行标记,清扫的时候扫描 span,没有标记的 span 就可以回收了。
go
// sweeps one span
// returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep
func sweepone() uintptr {
...
for {
s := mheap_.sweepSpans[1-sg/2%2].pop()
...
if !s.sweep(false) {
// Span is still in-use, so this returned no
// pages to the heap and the span needs to
// move to the swept in-use list.
npages = 0
}
}
}
// Sweep frees or collects finalizers for blocks not marked in the mark phase.
// It clears the mark bits in preparation for the next GC round.
// Returns true if the span was returned to heap.
// If preserve=true, don't return it to heap nor relink in MCentral lists;
// caller takes care of it.
func (s *mspan) sweep(preserve bool) bool {
...
}
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!