根底
存储金字塔
- CPU寄存器
- CPU Cache:三级Cache别离是L1、L2、L3,L1最快,L3最慢
- 内存
- 硬盘等辅助存储设备
- 鼠标等外接设备
从上至下的访问速度越来越慢,拜访工夫越来越长。
虚拟内存
拜访内存,理论拜访的是虚拟内存,虚拟内存通过页表查看,以后要拜访的虚拟内存地址,是否曾经加载到了物理内存。如果曾经在物理内存,则取物理内存数据,如果没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。
物理内存就是磁盘存储缓存层,在没有虚拟内存的时代,物理内存对所有过程是共享的,多过程同时拜访同一个物理内存会存在并发问题。而引入虚拟内存后,每个过程都有各自的虚拟内存,内存的并发拜访问题的粒度从多过程级别,能够升高到多线程级别。
栈和堆
代码中应用的内存地址都是虚拟内存地址,而不是理论的物理内存地址。栈和堆只是虚拟内存上2块不同性能的内存区域:
- 栈在高地址,从高地址向低地址增长
- 堆在低地址,从低地址向高地址增长
栈和堆相比有这么几个益处:
- 栈的内存治理简略,调配比堆上快。
- 栈的内存不须要回收,而堆须要进行回收,无论是被动free,还是被动的垃圾回收,这都须要破费额定的CPU。
- 栈上的内存有更好的局部性,堆上内存拜访就不那么敌对了,CPU拜访的2块数据可能在不同的页上,CPU拜访数据的工夫可能就下来了。
内存分区
Golang内存分区:代码区、数据区、堆区、栈区
// 低地址 ——----------------------------------------------------------------》 高地址
// 代码区 | 数据区(初始化数据区,未初始化数据区,常量区) | 堆区 | 栈区(函数信息,外部变量)
// 函数地址(0x7c7620):代码区。是一个低地址地位,计算机指令
// 全局变量(0xd03250) :初始化数据区,如果初始化了:初始化数据;未初始化:未初始化数据
// 局部变量(0xc0000120b0):栈区,高地址
// 堆区:一个很大的空间,在应用时,开拓内存空间,完结时,开释内存空间。
// 栈区:用来存储程序执行过程中函数外部定义的信息和局部变量值。
最内层函数后进先出,最内层函数先执行后,开释内存,向下层传递后果。 函数return返回值将函数执行的后果保留下来,返回给调用者。
变量
局部变量
- 在C语言中写在{}中或者函数中或者函数的形参, 就是局部变量
- Go语言中的局部变量和C语言一样
全局变量
- 在C语言中写在函数里面的就是全局变量
- Go语言中的全局变量和C语言一样
局部变量和全局变量的作用域
- 在C语言中局部变量的作用域是从定义的那一行开始, 直到遇到 } 完结或者遇到return为止
- Go语言中局部变量的作用域和C语言一样
- 在C语言中全局变量的作用域是从定义的那一行开始, 直到文件开端为止
- Go语言中的全局变量, 只有定义了, 在定义之前和定义之后都能够应用
局部变量和全局变量的生命周期
- 在C语言中局部变量, 只有执行了才会调配存储空间, 只有来到作用域就会主动开释, C语言的局部变量存储在栈区
- Go语言局部变量的生命周期和C语言一样
- 在C语言中全局变量, 只有程序一启动就会调配存储空间, 只有程序敞开才会开释存储空间, C语言的全局变量存储在动态区(数据区)
- Go语言全局变量的生命周期和C语言一样
局部变量和全局变量的留神点
- 在C语言中雷同的作用域内, 不能呈现同名的局部变量
- Go语言和C语言一样, 雷同干的作用域内, 不能呈现同名的局部变量
package main
import "fmt"
func main() {
var num int; // 局部变量
//var num int; // 报错,不能呈现同名局部变量
}
- 在C语言中雷同的作用域内, 能够呈现同名的全局变量
- 在Go语言中雷同的作用域内, 不能呈现同名的全局变量
例:
package main
import "fmt"
var value int // 全局变量
//var value int // 报错,不能呈现同名全局变量
func main() {
}
非凡点
- 在C语言中局部变量没有初始化存储的是垃圾数据, 在Go语言中局部变量没有初始化, 会默认初始化为0
- 在C语言中全局变量没有初始化存储的是0, Go语言和C语言一样
- 在Go语言中, 如果定义了一个局部变量, 然而没有应用这个局部变量, 编译会报错
- 在Go语言中, 如果定义了一个全局变量, 然而没有应用这个全局变量, 编译不会报错
留神点
- 雷同的作用域内, 无论是全局变量还是局部变量, 都不能呈现同名的变量
- 变量来到作用域就不能应用
- 局部变量如果没有应用, 编译会报错, 全局变量如果没有应用, 编译不会报错
- :=只能用于局部变量, 不能用于全局变量
- :=如果用于同时定义多个变量, 会有进化赋值景象,如果通过:=定义多个变量, 然而多个变量中有的变量曾经在后面定义过了, 那么只会对没有定义过的变量执行:=, 而定义过的变量只执行=操作
堆内存治理
内存调配 Malloc : memory allocator
当咱们说内存治理的时候,次要是指堆内存的治理,因为栈的内存治理不须要程序去操心。
当发现内存申请的时候,堆内存就会从未分配内存宰割出一个小内存块(block),而后用链表把所有内存块连接起来。须要一些信息形容每个内存块的根本信息,比方大小(size)、是否应用中(used)和下一个内存块的地址(next),内存块理论数据存储在data中。
一个内存块蕴含了3类信息:元数据、用户数据和对齐字段。
开释内存本质是把应用的内存块从链表中取出来,而后标记为未应用,当分配内存块的时候,能够从未应用内存块中优先查找大小相近的内存块,如果找不到,再从未调配的内存中分配内存。
TCMalloc (Thread Cache Malloc)
TCMalloc是 Google 开发的内存分配器,Golang 应用了相似的算法进行内存调配。
同一过程下的所有线程共享雷同的内存空间,它们申请内存时须要加锁,如果不加锁就存在同一块内存被2个线程同时拜访的问题。
TCMalloc的做法是什么呢?为每个线程预调配一块缓存,线程申请小内存时,能够从缓存分配内存,这样有2个益处:
- 为线程预调配缓存须要进行1次零碎调用,后续线程申请小内存时间接从缓存调配,都是在用户态执行的,没有了零碎调用,缩短了内存总体的调配和开释工夫,这是疾速分配内存的第二个档次。
- 多个线程同时申请小内存时,从各自的缓存调配,拜访的是不同的地址空间,从而无需加锁,把内存并发拜访的粒度进一步升高了,这是疾速分配内存的第三个档次。
基本原理
page
- 操作系统对内存治理以页为单位
- TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系
- x64下Page大小是8KB。
Span
- 一组间断的Page被称为Span,比方能够有2个页大小的Span,也能够有16页大小的Span
- Span比Page高一个层级,是为了方便管理肯定大小的内存区域
- Span是TCMalloc内存治理的根本单位
ThreadCache
- ThreadCache是每个线程各自的Cache
- 一个Cache蕴含多个闲暇内存块链表,每个链表连贯的都是内存块,同一个链表上内存块的大小是雷同的
- 这样能够依据申请的内存大小,疾速从适合的链表抉择闲暇内存块。因为每个线程有本人的ThreadCache
- ThreadCache拜访是无锁的
CentralCache
- CentralCache是所有线程共享的缓存,也是保留的闲暇内存块链表,链表的数量与ThreadCache中链表数量雷同
- 当ThreadCache的内存块有余时,能够从CentralCache获取内存块;当ThreadCache内存块过多时,能够放回CentralCache。
- 因为CentralCache是共享的,所以它的拜访是要加锁的。
PageHeap
- PageHeap是对堆内存的形象,PageHeap存的也是若干链表,链表保留的是Span。
- 当CentralCache的内存不足时,会从PageHeap获取闲暇的内存Span,而后把1个Span拆成若干内存块,增加到对应大小的链表中并分配内存;
- 当CentralCache的内存过多时,会把闲暇的内存块放回PageHeap中。
- 能够有是1页Page的Span链表,2页Page的Span链表等,最初是large span set,这个是用来保留中大对象的。
- PageHeap也是要加锁的。
TCMalloc对象大小的定义:
- 小对象大小:0~256KB
- 中对象大小:257KB~1MB
- 大对象大小:>1MB
对象调配流程:
-
小对象的调配流程:
- ThreadCache -> CentralCache -> HeapPage
- 大部分时候,ThreadCache缓存都是足够的,不须要去拜访CentralCache和HeapPage,无零碎调用配合无锁调配,调配效率是十分高的。
- 中对象调配流程:间接在PageHeap中抉择适当的大小即可,128 Page的Span所保留的最大内存就是1MB。
- 大对象调配流程:从large span set抉择适合数量的页面组成span,用来存储数据。
Go内存构造
Go在程序启动的时候,会先向操作系统申请一块内存(留神这时还只是一段虚构的地址空间,并不会真正地分配内存),切成小块后本人进行治理。
申请到的内存块被调配了三个区域,在X64上别离是512MB,16GB,512GB大小。
arena
arena8KBmspan
bitmap
bitmap区域arena4bitGC
bitmapbytearenabitmap512GB/(4*8B)=16GB
spans
spans区域mspanarenaspans512GB/8KB*8B=512MBarenaspansmspanspansobjectmspan
Go内存治理
GO比TCMalloc还多了2件货色:逃逸剖析和垃圾回收
基本概念
page
与TCMalloc中的Page雷同
span
- 与TCMalloc中的Span雷同,代码中为mspan
- Span是内存治理的根本单位
mcache
mcache 是提供给 P 的本地内存池。
mcache与TCMalloc中的ThreadCache相似,mcache保留的是各种大小的Span,并按Span class分类,小对象间接从mcache分配内存,它起到了缓存的作用,并且能够无锁拜访。
不同点:
- TCMalloc中是每个线程1个ThreadCache,Go中是每个P领有1个mcache
- 因为在Go程序中,以后最多有GOMAXPROCS个线程在运行,所以最多须要GOMAXPROCS个mcache就能够保障各线程对mcache的无锁拜访,线程的运行又是与P绑定的,把mcache交给P刚刚好。
mcentral
mcentral与TCMalloc中的CentralCache相似,是所有线程共享的缓存,须要加锁拜访。它按Span级别对Span分类,而后串联成链表,当mcache的某个级别Span的内存被调配光时,它会向mcentral申请1个以后级别的Span。
不同点:
- CentralCache是每个级别的Span有1个链表
- mcentral是每个级别的Span有2个链表
mheap
mheap_mheap
- mheap与TCMalloc中的PageHeap相似,它是堆内存的形象,把从OS申请出的内存页组织成Span,并保存起来。
- 当mcentral的Span不够用时会向mheap申请内存,而mheap的Span不够用时会向OS申请内存。
- mheap向OS的内存申请是按页来的,而后把申请来的内存页生成Span组织起来,同样也是须要加锁拜访的。
不同点:
- mheap把Span组织成了树结构,而不是链表,并且还是2棵树
- mheap把Span调配到heapArena进行治理,它蕴含地址映射和span是否蕴含指针等位图,这样做的次要起因是为了更高效的利用内存:调配、回收和再利用。
GO内存大小转化
- object size:代码里简称size,指申请内存的对象大小。
-
size class:代码里简称class,它是size的级别,相当于把size归类到肯定大小的区间段
- size[1,8]属于size class 1
- size(8,16]属于size class 2
- size(16,32]属于size class 3
- size(32,48]属于size class 4
- span class:指span的级别,但span class的大小与span的大小并没有反比关系。span class次要用来和size class做对应,1个size class对应2个span class,2个span class的span大小雷同,只是性能不同,1个用来寄存蕴含指针的对象,一个用来寄存不蕴含指针的对象,不蕴含指针对象的Span就无需GC扫描了。
- num of page:代码里简称npage,代表Page的数量,其实就是Span蕴含的页数,用来分配内存。
class 1 2 3 4 5 6 ··· 63 64 65 66
bytes 8 16 32 48 64 80 ··· 24576 27264 28672 32768
Go内存调配
mcachemcentralmheap
内存分类
- 当要调配大于 32K 的对象时,从 mheap 调配。
- 当要调配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 调配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。
- 当要调配的对象小于等于 16B 时,从 mcache 上的微型分配器上调配。
大小对象
-
小对象:小对象是在mcache中调配的
- Tiny对象:大小在1~16Byte之间并且不蕴含指针的对象
- 其余小对象: 16Byte~32KB
- 大对象:大于32KB,间接从mheap调配
小对象内存调配
- size class数量:_NumSizeClasses=67
- span class数量:numSpanClasses = _NumSizeClasses * 2 = 134
- 也就是mcache最多有134个span
1. 为对象寻找span:
- 计算对象所需内存大小size
- 依据size到size class映射,计算出所需的size class
- 依据size class和对象是否蕴含指针计算出span class
- 获取该span class指向的span
- 举例:24Byte对象属于size class 3,对应的span class为7
2. 从span调配对象空间
- Span能够按对象大小切成很多份:以size class 3对应的span为例,span大小是8KB,每个对象理论所占空间为32Byte,这个span就被分成了256块。
- 随着内存的调配,span中的对象内存块,有些被占用,有些未被占用,当分配内存时,只有疾速找到第一个可用的绿色块,并计算出内存地址即可。
- 当span内的所有内存块都被占用时,没有残余空间持续调配对象,mcache会向mcentral申请1个span,mcache拿到span后持续调配对象。
3. mcache向mcentral申请span
mcentral和mcache一样,都是0~133这134个span class级别,但每个级别都保留了2个span list,即2个span链表:
- nonempty:这个链表里的span,所有span都至多有1个闲暇的对象空间。这些span是mcache开释span时退出到该链表的。
- empty:这个链表里的span,所有的span都不确定外面是否有闲暇的对象空间。当一个span交给mcache的时候,就会退出到该链表
mcache向mcentral申请span时,mcentral会先从nonempty搜寻满足条件的span,如果没有找到再从emtpy搜寻满足条件的span,而后把找到的span交给mcache。
4. mheap的span治理
mheap里保留了两棵二叉排序树,按span的page数量进行排序:
- free:free中保留的span是闲暇并且非垃圾回收的span。
- scav:scav中保留的是闲暇并且曾经垃圾回收的span。
如果是垃圾回收导致的span开释,span会被退出到scav,否则退出到free,比方刚从OS申请的的内存也组成的Span。
mheap中还有arenas(动态分配的堆区),由一组heapArena组成,每一个heapArena都蕴含了间断的pagesPerArena个span,这个次要是为mheap治理span和垃圾回收服务。arenas自身是一个全局变量,它外面的数据,也都是从OS间接申请来的内存,并不在mheap所治理的那局部内存以内。
5. mcentral向mheap申请span
当mcentral向mcache提供span时,如果empty里也没有符合条件的span,mcentral会向mheap申请span。
此时,mcentral须要向mheap提供须要的内存页数和span class级别,而后它优先从free中搜寻可用的span。如果没有找到,会从scav中搜寻可用的span。如果还没有找到,它会向OS申请内存,再从新搜寻2棵树,必然能找到span。
如果找到的span比须要的span大,则把span进行宰割成2个span,其中1个刚好是需要大小,把剩下的span再退出到free中去,而后设置须要的span的根本信息,而后交给mcentral。
6. mheap向OS申请内存
当mheap没有足够的内存时,mheap会向OS申请内存,把申请的内存页保留为span,而后把span插入到free树。此时,mcentral须要向mheap提供须要的内存页数和span class级别,而后它优先从free中搜寻可用的span。如果没有找到,会从scav中搜寻可用的span。如果还没有找到,它会向OS申请内存,再从新搜寻2棵树,必然能找到span。
如果找到的span比须要的span大,则把span进行宰割成2个span,其中1个刚好是需要大小,把剩下的span再退出到free中去,而后设置须要的span的根本信息,而后交给mcentral。
大对象内存调配
当要调配大于 32K 的对象时,从 mheap 调配。
大对象的调配比小对象省事多了,99%的流程与mcentral向mheap申请内存的雷同,所以不反复介绍了。不同的一点在于mheap会记录一点大对象的统计信息,详情见mheap.alloc_m()。
垃圾回收和内存开释
- 垃圾回收收集不再应用的span,调用mspan.scavenge()把span开释还给OS(并非真开释,只是通知OS这片内存的信息无用了,如果你需要的话,发出去好了)
- 而后交给mheap,mheap对span进行span的合并,把合并后的span退出scav树中
- 期待再分配内存时,由mheap进行内存再调配
栈内存
每个goroutine都有本人的栈,栈的初始大小是2KB,100万的goroutine会占用2G,但goroutine的栈会在2KB不够用时主动扩容,当扩容为4KB的时候,百万goroutine会占用4GB。
应用程序的内存会分成堆区(Heap)和栈区(Stack)两个局部,程序在运行期间能够被动从堆区申请内存空间,这些内存由内存分配器调配并由垃圾收集器负责回收。
栈区的内存由编译器主动进行调配和开释,栈区中存储着函数的参数以及局部变量,它们会随着函数的创立而创立,函数的返回而销毁。
go语言编译器会主动决定把一个变量放在栈还是放在堆,编译器会做逃逸剖析(escape analysis),当发现变量的作用域没有跑出函数范畴,就能够在栈上,反之则必须调配在堆。
总结
Go内存调配治理的策略有如下几点:
mheapmspanmspanobjectmcachemcentralmheapGomcachemspanmcentralmspanmheapGomcachemheap
Reference
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://blog.haohtml.com/arch…
https://zhuanlan.zhihu.com/p/…
https://blog.csdn.net/kevin_t…