根底

存储金字塔

  • 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. 为线程预调配缓存须要进行1次零碎调用,后续线程申请小内存时间接从缓存调配,都是在用户态执行的,没有了零碎调用,缩短了内存总体的调配和开释工夫,这是疾速分配内存的第二个档次。
  2. 多个线程同时申请小内存时,从各自的缓存调配,拜访的是不同的地址空间,从而无需加锁,把内存并发拜访的粒度进一步升高了,这是疾速分配内存的第三个档次。

基本原理

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内存大小转化

  1. object size:代码里简称size,指申请内存的对象大小。
  2. 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
  3. span class:指span的级别,但span class的大小与span的大小并没有反比关系。span class次要用来和size class做对应,1个size class对应2个span class,2个span class的span大小雷同,只是性能不同,1个用来寄存蕴含指针的对象,一个用来寄存不蕴含指针的对象,不蕴含指针对象的Span就无需GC扫描了。
  4. 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:
  1. 计算对象所需内存大小size
  2. 依据size到size class映射,计算出所需的size class
  3. 依据size class和对象是否蕴含指针计算出span class
  4. 获取该span class指向的span
  5. 举例: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链表:

  1. nonempty:这个链表里的span,所有span都至多有1个闲暇的对象空间。这些span是mcache开释span时退出到该链表的。
  2. empty:这个链表里的span,所有的span都不确定外面是否有闲暇的对象空间。当一个span交给mcache的时候,就会退出到该链表

mcache向mcentral申请span时,mcentral会先从nonempty搜寻满足条件的span,如果没有找到再从emtpy搜寻满足条件的span,而后把找到的span交给mcache。

4. mheap的span治理

mheap里保留了两棵二叉排序树,按span的page数量进行排序:

  1. free:free中保留的span是闲暇并且非垃圾回收的span。
  2. 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…