go 与 java类似, 都实现了自动内存管理。
Golang运行时的内存分配算法主要是 TCMalloc算法。 核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
mSpan
使用空闲(free)
、已分配(busy
)和垃圾回收(gc)
。空闲 mSpan 表示其中的 Page 未被分配;已分配 mSpan 表示其中的 Page 已被分配给对象;垃圾回收 mSpan 表示正在等待垃圾回收器处理的内存区域。4. SizeClass
是指内存分配时根据对象大小选择的一组预定义的类别,例如8B、16B、32B等等。Go的内存分配器使用大小类别来决定如何分配和管理内存。每个大小类别对应一个特定的对象大小范围。这样做的目的是为了减少内存碎片并提高内存分配的效率。
是指 mSpan 的一个属性,它描述了 mSpan 的用途和特性。SpanClass 与 SizeClass 相关联,因为它决定了 mSpan 如何被用于特定大小类别的对象分配。 SpanClass通常包含两个部分的信息:
SizeClass 和 SpanClass 之间的关系在于,SizeClass 决定了对象的大小范围,而 SpanClass 决定了哪个 mSpan 应该被用来分配这个大小范围内的对象。SpanClass 实际上是对 SizeClass 的一种补充,它帮助内存分配器在内部管理和组织 mSpan。
基本类型内存占用:
int8、uint8、byte、bool 占用 1B
int16、uint16 占用 2B
int32、uint32、float32、rune 占用 4B
int64、uint64、float64 占用 8B
int 和 uint 具体看 CPU
32位 CPU 占用 4B,等同于 int32
64位 CPU 占用 8B,等同于 int64
现在基本都是 64位 CPU,因此 int 基本上等同于 int64
uintptr 一般情况下,32位 是 4B,64 位是 8B
string 底层实现类似结构体,由两部分组成,底层是
type StringHeader struct {
Data uintptr; // 数据所在的内存地址,8B
Len int; // 字符串产固定, 8B
}
因此 string 类型占用大小为 16B,结构体中存储的是 string 这个结构体,而不是真实数据
切片底层跟 string 也是一个类似结构体的结构,
type slice struct {
Data uintptr; // 数据所在的内存地址,8B
Len int; // 字符串产固定, 8B
}
因此结构体中存储的切片占用内存固定位 16B,因为不存储真实数据
结构体内存占用:
type T struct {
a bool //1
b int32 //4
c int8 //1
d int64 //8
e byte //1
// 15
}
func main() {
var t T
fmt.Printf("a: %d b: %d c: %d d: %d e: %d\n",
unsafe.Sizeof(t.a), unsafe.Sizeof(t.b), unsafe.Sizeof(t.c), unsafe.Sizeof(t.d), unsafe.Sizeof(t.e))
fmt.Printf("所有变量理论占用内存大小:%d\n",
unsafe.Sizeof(t.a)+unsafe.Sizeof(t.b)+unsafe.Sizeof(t.c)+unsafe.Sizeof(t.d)+unsafe.Sizeof(t.e))
fmt.Printf("实际结构体占用内存大小 sizeof:%d\n", unsafe.Sizeof(t)) // 8
}
// ======= 整体输出
a: 1 b: 4 c: 1 d: 8 e: 1
所有变量理论占用内存大小:15
实际结构体占用内存大小 sizeof:32
从上可以看出, 虽然理论占用 15B, 实际上却占用了 32B, 这就是编译器会对结构体进行内存对齐
结构体中变量的内存地址不是随意分配的,它是根据 对齐保证 作为依据来进行分配的, 通过 unsafe.Alignof()
可以获取对齐保证的值。
内存对齐保证规则:
一般而言,普通数据类型的对齐保证是它的内存大小,结构体的对齐保证是它内部所有变量中的最大的对齐保证
type T struct {
a bool //1
b int32 //4
c int8 //1
d int64 //8
e byte //1
// 15
}
func main() {
var t T
fmt.Printf("t 中实际占用内存大小:%d, 对齐保证:%d\n", unsafe.Sizeof(t), unsafe.Alignof(t))
fmt.Printf("a 占用内存大小:%d, 对齐保证:%d, 内存地址偏移量:%d\n", unsafe.Sizeof(t.a), unsafe.Alignof(t.a), unsafe.Offsetof(t.a))
fmt.Printf("b 占用内存大小:%d, 对齐保证:%d, 内存地址偏移量:%d\n", unsafe.Sizeof(t.b), unsafe.Alignof(t.b), unsafe.Offsetof(t.b))
fmt.Printf("c 占用内存大小:%d, 对齐保证:%d, 内存地址偏移量:%d\n", unsafe.Sizeof(t.c), unsafe.Alignof(t.c), unsafe.Offsetof(t.c))
fmt.Printf("d 占用内存大小:%d, 对齐保证:%d, 内存地址偏移量:%d\n", unsafe.Sizeof(t.d), unsafe.Alignof(t.d), unsafe.Offsetof(t.d))
fmt.Printf("e 占用内存大小:%d, 对齐保证:%d, 内存地址偏移量:%d\n", unsafe.Sizeof(t.e), unsafe.Alignof(t.e), unsafe.Offsetof(t.e))
}
// ======= 整体输出 =======
t 中实际占用内存大小:32, 对齐保证:8
a 占用内存大小:1, 对齐保证:1, 内存地址偏移量:0 // 第一个变量的偏移量永远是 0,不需要是对齐保证的整数倍
// a 内存占用大小为 1,内存偏移量为 0,那么理论上下一个变量的内存地址偏移量应该为 1,但实际上 b 的内存地址偏移量为 4
// 这是因为 b 占用内存大小为 4,而内存大小必须是对齐保证的整数倍,由于内存大小为 4,那么就意味着对齐保证必须是 4 以上
// 因此 b 的内存地址偏移量也必须是 4 以上,那么满足要求的最小内存地址偏移量就是 4 了,因此 b 的内存地址偏移量不是 1 而是 4
// 也因此,导致 [1, 3] 之间的内存是空余的,浪费了 3byte
b 占用内存大小:4, 对齐保证:4, 内存地址偏移量:4
// c 内存地址偏移量为 8 是因为 b 内存大小 + b 内存地址偏移量 = 8,所以没问题
c 占用内存大小:1, 对齐保证:1, 内存地址偏移量:8
// c 占用内存为 1,内存偏移量为 8,那么理论上下一个变量的内存地址偏移量应该为 9,但实际上 b 的内存地址偏移量为 16
// 理由同 b
// 因此导致 [9, 15] 间的内存是空余的,浪费了 7byte
d 占用内存大小:8, 对齐保证:8, 内存地址偏移量:16
// 内存占用没问题,内存地址偏移量也是根据 d 来计算的
// 最终总的内存大小占用为 25
// 但是因为结构体的对齐保证为 8,因此结构体整体的内存占用大小必须是 8 的整数倍,因此需要再填充 7byte,变成 32
e 占用内存大小:1, 对齐保证:1, 内存地址偏移量:24
在Go语言的内存管理系统中,MCache、MCentral和堆栈内存是构成Go内存分配器的关键组件:
这些组件之间的关系可以概括如下:
在 go 里面对象分为三类:
go 在 V1.3 版本用的是 标记-清除, 与 java 类似, 流程是:
标记-清除机制最大的缺点就是 STW 会让程序暂停, 对性能有严重的影响(包括 Java 的 CMS 也是让 STW 的时间尽量变短)
从 V1.5 开始, GO 改为了三色标记法, 主要思想就是:
但是上述的三色标记法依然是依赖 STW 的, 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性。 比如:
不过这种标记过程中的对象引用关系丢失, 需要满足两个条件:
只要把上面两个条件破坏掉一个,就可以保证对象不丢失,所以我们的golang团队就提出了两种破坏条件的方式:强三色不变式和弱三色不变式.
Golang团队遵循上述两种不变式提到的原则,分别提出了两种实现机制:插入写屏障和删除写屏障。
规则:当一个对象引用另外一个对象时,将另外一个对象标记为灰色 (满足强三色不变式) 解释: 不会存在黑色对象引用白色对象
这里需要注意一点,插入屏障仅会在堆内存中生效,不对栈内存空间生效,这是因为go在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。数十万goroutine的栈都进行屏障保护自然会有性能问题。
由于插入写屏障对栈上的对象不生效, 所以他有一个弊端: 在一次正常的三色标记流程结束后,需要对栈上重新进行一次stw,然后再rescan一次
在删除引用时,如果被删除引用的对象自身为灰色或者白色,那么被标记为灰色(满足弱三色不变式) 解释: 白色对象始终会被灰色对象保护,灰色对象到白色对象的路径不会断
但是引入删除写屏障,有一个弊端,就是一个对象的引用被删除后,即使没有其他存活的对象引用它,它仍然会活到下一轮。如此一来,会产生很多的冗余扫描成本,且降低了回收精度,举例来讲
从上面示例来看,插入写屏障机制和删除写屏障机制中任一机制均可保护对象不被丢失。在V1.5的版本中采用的是插入写机制实现
对比插入写屏障和删除写屏障:
从上面来看插入写屏障和删除写屏障各有优劣, go 团队结合两者的特点, 在v1.8版本下引入了混合写屏障机制。下面我们看下混合屏障机制的核心定义: