go内存分配(下)
栈
栈区的内存一般是由编译器自动进行分配和释放,其中存储着函数的入参及局部变量,这些参数会随着函数的创建而创建,函数的销毁而销毁。
堆
堆区的内存一般有编译器和工程师自己共同进行管理分配,交给runtimeGc来释放。堆上分配必须找到一块足够大的内存来存放新的变量数据。后续释放时,垃圾回收器扫描堆空间寻找不再被使用的对象。
内存基础
内存: cpu与硬盘速度不匹配,引入内存作为中间缓冲。
cache: cpu与内存速度也不匹配,引入chache作为中间缓冲,并逐渐发展为3级cache。L1、L2、L3。其中l1速度最快
虚拟内存: 虚拟内存是当代操作系统必备的一项重要功能了,它向进程屏蔽了底层了RAM和磁盘,并向进程提供了远超物理内存大小的内存空间。
进程访问数据,当Cache没有命中的时候,访问虚拟内存获取数据,当前要访问的虚拟内存地址,是否已经加载到了物理内存,如果已经在物理内存,则取物理内存数据,如果没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。
在没有虚拟内存的时代,物理内存对所有进程是共享的,多进程同时访问同一个物理内存存在并发访问问题。引入虚拟内存后,每个进程都要各自的虚拟内存,内存的并发访问问题的粒度从多进程级别,可以降低到多线程级别。
堆和栈: 虚拟内存中的栈和堆,也就是进程对内存的管理。 栈和堆相比有这么几个好处:
- 栈的内存管理简单,分配比堆上快。
- 栈的内存不需要回收,而堆需要,无论是主动free,还是被动的垃圾回收,这都需要花费额外的CPU。
TCMalloc
TCMalloc是go内存分配的前辈,Go的内存管理是借鉴了TCMalloc,随着Go的迭代,Go的内存管理与TCMalloc不一致地方在不断扩大,但其主要思想、原理和概念都是和TCMalloc一致的。
引入虚拟内存后,让内存的并发访问问题的粒度从多进程级别,降低到多线程级别。但是同一个进程中多个线程申请内存时需要加锁,如果不加锁就存在同一块内存被2个线程同时访问的问题。
TCMalloc的做法是什么呢?为每个线程预分配一块缓存,线程申请小内存时,可以从缓存分配内存,这样有2个好处:
- 以后线程在再分配内存就是在用户态,不需要系统调用即可。
- 线程利用自己的内存缓存,不用加锁了。
TCMalloc的几个重要概念
Page:操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,x64下Page大小是8KB。
Span:一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
ThreadCache:每个线程各自的Cache。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
CentralCache:是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。
PageHeap:PageHeap是堆内存的抽象,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。
TCMalloc中有小、中、大对象概念,Go内存管理中也有类似的概念,我们瞄一眼TCMalloc的定义:
- 小对象大小:0~256KB
- 中对象大小:257~1MB
- 大对象大小:>1MB
小对象的分配流程:ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache缓存都是足够的,不需要去访问CentralCache和HeapPage,无锁分配加无系统调用,分配效率是非常高的。
中对象分配流程:直接在PageHeap中选择适当的大小即可,128 Page的Span所保存的最大内存就是1MB。
大对象分配流程:从large span set选择合适数量的页面组成span,用来存储数据。
Go内存管理
通过TCMalloc的了解,可以看出他的精髓是内存的分层。go借鉴了他。
go内存的概念
Page:与TCMalloc中的Page相同,x64下1个Page的大小是8KB。
pan:与TCMalloc中的Span相同,Span是内存管理的基本单位,代码中为mspan,一组连续的Page组成1个Span。
mcache:mcache与TCMalloc中的ThreadCache类似,mcache保存的是各种大小的Span,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问。
但mcache与ThreadCache也有不同点,TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcache,因为在Go程序中,当前最多有GOMAXPROCS个线程在用户态运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问,线程的运行又是与P绑定的,把mcache交给P刚刚好。
mcentral:mcentral与TCMalloc中的CentralCache类似,是所有线程共享的缓存,需要加锁访问,它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。
mheap:mheap与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS申请出的内存页组织成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请。
对象的分级
为了更好的应对不同大小的对象的分配,有了class的概念,一共67中class,每种class所拥有的page数量是不同的。size class 0实际并未使用到。
object size:代码里简称size,指申请内存的对象大小。
size class:代码里简称class,它是size的级别,相当于把size归类到一定大小的区间段,比如size[1,8]属于size class 1,size(8,16]属于size class 2。
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包含的页数,用来分配内存。
go内存分配
Go中的内存分类并不像TCMalloc那样分成小、中、大对象,但是它的小对象里又细分了一个Tiny对象,Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象。小对象和大对象只用大小划定,无其他区分。
小对象是在mcache中分配的,而大对象是直接从mheap分配的。
小对象分配
寻找span的流程如下:
- 计算对象所需内存大小size
- 根据size到size class映射,计算出所需的size class
- 根据size class和对象是否包含指针计算出span class
- 获取该span class指向的span。
对象size-> size class -> span class -> span class -> span -> 从span分配地址。
大对象分配
大对象的分配比小对象省事多了,99%的流程与mcentral向mheap申请内存的相同,所以不重复介绍了,不同的一点在于mheap会记录一点大对象的统计信息。