深刻理解GO语言以内存详解

一,前言

深刻学习golang,必需要了解内存这块,此次会仔细讲解下内存这块,包括内存分配,内存模型,逃逸分析。让咱们在编程中能注意下这块。golang

二,内存分配

(1) 这里先了解四个相关数据结构

1,mspan

经过next和prev,组成一个双向链表,mspan负责管理从startAddr开始的N个page的地址空间。是基本的内存分配单位。是一个管理内存的基本单位。编程

//保留重要成员变量
type mspan struct {
	next *mspan     // 链表中下个span
	prev *mspan     // 链表中上个span	startAddr uintptr // 该mspan的起始地址
	freeindex uintptr // 表示分配到第几个块
	npages    uintptr // 一个span中含有几页
	sweepgen    uint32 // GC相关
	incache     bool       // 是否被mcache占用
	spanclass   spanClass  // 0 ~ _NumSizeClasses之间的一个值,好比,为3,那么这个mspan被分割成32byte的块
}
复制代码

2,mcache

在go中,每一个P都会被分配一个mcache,是私有的,从这里分配内存不须要加锁数组

type mcache struct {
	tiny             uintptr // 小对象分配器
	tinyoffset       uintptr // 小对象分配偏移
	local_tinyallocs uintptr // number of tiny allocs not counted in other stats
	alloc [numSpanClasses]*mspan // 存储不一样级别的mspan
}
复制代码

3,mcentral

当mcache不够时候,会向mcentral申请内存。该结构其实是在mheap中的,因此在我看来,这起到桥梁的做用。缓存

type mcentral struct {
	lock      mutex    // 多个P会访问,须要加锁
	spanclass spanClass  // 对应了mspan中的spanclass
	nonempty  mSpanList // 该mcentral可用的mspan列表
	empty     mSpanList // 该mcentral中已经被使用的mspan列表
}
复制代码

4,mheap

mheap是真实拥有虚拟地址的,当mcentral不够时候,会向mheap申请。bash

type mheap struct {
	lock      mutex                    // 是公有的,须要加锁
	free      [_MaxMHeapList]mSpanList // 未分配的spanlist,好比free[3]是由包含3个 page 的 mspan 组成的链表	
	freelarge mTreap                   // mspan组成的链表,每一个mspan的 page 个数大于_MaxMHeapList
	busy      [_MaxMHeapList]mSpanList // busy lists of large spans of given length
	busylarge mSpanList                // busy lists of large spans length >= _MaxMHeapList
	allspans []*mspan                  // 全部申请过的 mspan 都会记录在 allspans
	spans []*mspan                     // 记录 arena 区域页号(page number)和 mspan 的映射关系

	arena_start uintptr // arena是Golang中用于分配内存的连续虚拟地址区域,这是该区域开始的指针
	arena_used  uintptr // 已经使用的内存的指针
	arena_alloc uintptr
	arena_end   uintptr

	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte //避免伪共享(false sharing)问题
	}
	
	spanalloc             fixalloc // allocator for span*
	cachealloc            fixalloc // mcache分配器

}

复制代码

接下来请细看下图,结合前面的讲解进行理解(务必看懂)。数据结构

(2) 内存分配细节

这里不展开源代码,知道分配规则便可。(在golang1.10,MacOs 10.12中,下面的32K改成64K)多线程

1, object size > 32K;则使用 mheap 直接分配。app

2,object size < 16 byte;则使用 mcache 的小对象分配器 tiny 直接分配。函数

3,object size > 16 byte && size <= 32K byte时,先在mcache申请分配。若是 mcache对应的已经没有可用的块,则向mcentral请求,若是mcentral也没有可用的块,则向mheap申请,若是 mheap 也没有合适的span,则向操做系统申请。学习

三,内存模型

这里说下在golang中的happen-before(假设A和B表示一个多线程的程序执行的两个操做。若是A happens-before B,那么A操做对内存的影响将对执行B的线程(且执行B以前)可见)

(1) Init 函数

1, P1中导入了包P2,则P2中的init函数Happens BeforeP1中全部的操做

2, 全部的init函数Happens Before Main函数

(2) Channel

1, 对一个元素的send操做Happens Before对应的receive操做

2, 对channel的close操做Happens Before receive端的收到关闭通知操做

3, 对于无缓存的Channel,对一个元素的receive 操做Happens Before对应的send完成操做

4, 对于带缓存的Channel,假设Channel 的buffer 大小为C,那么对第k个元素的receive操做,Happens Before第k+C个send完成操做。 。

四,逃逸分析

为何要作逃逸分析呢,由于在栈上分配的代价要远小于在堆上进行分配,这块是目前不少人缺少的一个思惟,包括我。最近看了一些这方面的文章,再回去看本身的代码,发现不少不合理的地方,但愿经过此次讲解,能一块儿进步。

(1) 什么是内存逃逸

简单来讲就是本来应在栈上分配内存的对象,逃逸到了堆上进行分配。若是能在栈上进行分配,那么只须要两个指令,入栈和出栈,GC压力也小了。因此相比之下,在栈上分配代价会小不少。

(2) 引发逃逸的状况

我的总结了一下,若是没法在编译期肯定变量的做用域和占用内存大小,则会逃逸到堆上。

1,指针

咱们平时会知道,传递指针能够减小底层值的拷贝,能够提升效率,在通常状况下是如此,可是若是拷贝的是少许的数据,那么传递指针效率不必定会高于值拷贝。

(1) 指针是间接访址,所指向的地址大多保存在堆上,所以考虑到GC,指针不必定是高效的。看个例子

type test struct{}

func main() {
	t1 := test1()
	t2 := test2()
	println("t1", &t1, "t2", &t2)
}

func test1() test {
	t1 := test{}
	println("t1", &t1)
	return t1
}

func test2() *test {
	t2 := test{}
	println("t2", &t2)
	return &t2
}
复制代码

运行查看逃逸状况(禁止内联)

go run -gcflags '-m -l' main.go
# command-line-arguments
./main.go:36:16: test1 &t1 does not escape
./main.go:43:9: &t2 escapes to heap
./main.go:41:2: moved to heap: t2
./main.go:42:16: test2 &t2 does not escape
./main.go:31:16: main &t1 does not escape
./main.go:31:27: main &t2 does not escape
t1 0xc420049f50
t2 0x10c1648
t1 0xc420049f70 t2 0xc420049f70

复制代码

从上面能够看出,返回指针的test2函数中的t2逃逸到堆上,等待它的将是残忍的GC。

2,切片

若是编译期没法肯定切片的大小或者切片大小过大,超出栈大小限制,或者在append时候会致使从新分配内存,这时候极可能会分配到堆上。

// 切片超过栈大小
func main(){
	s := make([]byte, 1, 64 * 1024)
	_ = s
}

// 没法肯定切片大小
func main() {
	s := make([]byte, 1, rand2.Intn(10))
	_ = s
}
复制代码

看完上述的例子,咱们来看个有意思的例子。咱们知道,切片比数组高效,可是,确实是如此吗?

func array() [1000]int {
	var x [1000]int
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}

func slice() []int {
	x := make([]int, 1000)
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}

func BenchmarkArray(b *testing.B) {
	for i := 0; i < b.N; i++ {
		array()
	}
}

func BenchmarkSlice(b *testing.B) {
	for i := 0; i < b.N; i++ {
		slice()
	}
}
复制代码

运行结果以下

go test -bench . -benchmem -gcflags "-N -l -m"
BenchmarkArray-4        30000000                52.8 ns/op             0 B/op          0 allocs/op
BenchmarkSlice-4        20000000                82.4 ns/op           160 B/op          1 allocs/op
复制代码

可见,咱们不必定是必定要用切片代替数组,由于切片底层数组可能会在堆上分配内存,并且小数组在栈上拷贝的消耗也未必比切片大。

3,interface

interface是咱们在go中常常会用到的特性,很是好用,可是因为interface类型在编译期间,编译期很难肯定其具体类型,所以也致使了逃逸现象。举个最简单的例子

func main() {
	s := "abc"
	fmt.Println(s)
}
复制代码

上述代码会产生逃逸,缘由是fmt.Println这个方法接收的参数是interface类型。可是这块只是做为科普,毕竟interface带来的好处要大于它这个缺陷

五,参考文献

segment.com/blog/alloca…

相关文章
相关标签/搜索