根底
存储金字塔
- 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
arena
就是咱们所谓的堆区,Go 动态分配的内存都是在这个区域 ,它把内存宰割成8KB
大小的页,一些页组合起来称为mspan
bitmap
bitmap 区域
标识 arena
区域哪些地址保留了对象 ,并且用4bit
标记位示意对象是否蕴含指针、GC
标记信息。
bitmap
中一个 byte
大小的内存对应 arena
区域中 4 个指针大小(指针大小为 8B)的内存,所以 bitmap
区域的大小是512GB/(4*8B)=16GB
。
spans
spans 区域
寄存 mspan
的指针 (也就是一些arena
宰割的页组合起来的内存治理根本单元,后文会再讲),每个指针对应一页,所以 spans
区域的大小就是 512GB/8KB*8B=512MB
。除以 8KB 是计算arena
区域的页数,而最初乘以 8 是计算 spans
区域所有指针的大小。创立 mspan
的时候,按页填充对应的 spans
区域,在回收 object
时,依据地址很容易就能找到它所属的mspan
。
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
代表 Go 程序持有的所有堆空间,Go 程序应用一个 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 内存调配
内存调配由内存分配器实现。分配器由 3 种组件形成:mcache
, mcentral
, mheap
。
内存分类
- 当要调配大于 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 内存调配治理的策略有如下几点:
- Go 在程序启动时,会向操作系统申请一大块内存,由
mheap
构造全局治理。 - Go 内存治理的根本单元是
mspan
,每种mspan
能够调配特定大小的object
。 -
mcache
,mcentral
,mheap
是Go
内存治理的三大组件:mcache
治理线程在本地缓存的mspan
(无锁)mcentral
治理全局的mspan
供所有线程应用(有锁)mheap
治理Go
的所有动静分配内存。(有锁)
- Tiny 对象(0~16B 且无指针),个别小对象通过
mcache
分配内存(16B~32K);大对象则间接由mheap
分配内存(大于 32K)。
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…