内存是怎么分配给对象的?
内存分配优化的地方是?
讲讲golang内存分配模型?
ans:
1.按照对象的大小分配:先算出对象的大小如果是tiny对象,就从tiny block中获取地址和偏移量,将对象打包到mcache;如果是16B以上32k以内就先从mcache获得对应class级别的span;如果mcache没有就从mcenter中获取,如果mcenter没有就创建一个mspan从free树或scav树中获得内存页。如果堆中没有就系统调用申请内存,后再扫描树。
2.优化的地方是使用了类似于tcmalloc模型,向每个p预分配2k的缓存减少系统分配的次数且mcahe是p私有的不用加锁操作;
3.将page组织进span作为内存管理的基本单位;mcache中根据page的页数范围不同class的span链表;mcenter作为缓存池存放不同class的两个span链表,一个链表叫empty不确定里面是否有空闲的对象空间,另一个是noempty所有span都至少有1个空闲的对象空间.mheap存放两个二查排序空闲已回收树,free树:空闲未垃圾回收。scav树:空闲已垃圾回收。
面对三连问,你是不是和我一样懵逼了?以下是我查询资料学习总结的知识体系,一起看下去吧🙀
1.存储的基本知识
计算机存储体系:
cpu的运算速度很快,而其他的硬件速度慢,为了好好利用cpu的性能所以加入了高速缓存和层层缓存层。
引入虚拟缓存后,操作系统向进程屏蔽了物理物理,cpu查询内存如果高速cache没有命中就会查找页表从磁盘将物理内存加载进缓存更新页表映射。由于引入虚拟内存所以物理内存的并发访问问题的级别由进程级转为了多线程级别。
2.TCMalloc
2.1基础
学习内存先得了解两个内存区域:
栈内存:栈中的内存会自动回收,内存管理简单,分配快。
堆内存:堆上的内存需要手动分配和释放,浪费gc。
堆内存管理分为三个重要步骤:
分配内存、(使用内存)、回收内存、组织内存块
一个内存块中包含了:元数据、用户数据、对齐字段
分配:从堆中把内存分配出来,将信息存进内存块,将内存块组织为链表。
释放:从链表中的内存块取出标记为未使用。
分配顺序:从链表中获得未使用的内存块没有再从内存中分配。
2.2tcmalloc机制
引入TCmalloc
同一进程所有线程共享空间,为了更快的分配内存,为每个线程预分配一些内存,再申请小内存可以直接从缓存分配。因为只有一次系统调用且分配无需加锁,因此,可以达到快速分配的目的。
内存管理的基本单位是page,一组连续的page被称为span,tcCache是线程级别的缓存,包含有不同级别的span链表,centralCache是所有线程共享的缓存包含不同级别的span只是访问需要加锁,pageHeap是堆内存的抽象保存若干span链表和large span set保存中大对象.
小对象的分配流程:ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache缓存都是足够的,不需要去访问CentralCache和HeapPage,无锁分配加无系统调用,分配效率是非常高的。
中对象分配流程:直接在PageHeap中选择适当的大小即可,128 Page的Span所保存的最大内存就是1MB。
大对象分配流程:从large span set选择合适数量的页面组成span,用来存储数据。
3.Go的内存管理与分配
不同级别的内存管理对象:
page:8kb
span:内存管理的基本单位
mcahe:线程级别的缓存,无锁访问,mcache与p绑定
mcentral:线程共享的缓存,加锁访问,每个级别的span有有指针的链表和无指针的链表
mheap:从操作系统中分配的内存页按照span组织起来,组织形式是二叉排序树:
free树:空闲并未垃圾回收
scav:空闲已经垃圾回收
小对象是在mcache中分配的,而大对象是直接从mheap分配的,从小对象的内存分配看起。
不同大小的对象内存分配:
小对象:
- 计算对象所需内存size
- Size class和是否需要指针获得span class
- 获取对应class级别的cspan
a.mcache ->b. mcenteal ->c.mheap
span内的所有内存块都被占用时,没有剩余空间继续分配对象,mcache会向mcentral申请1个span,mcache拿到span后继续分配对象。
mcentral有两个链表。
- nonempty:这个链表里的span,所有span都至少有1个空闲的对象空间。这些span是mcache释放span时加入到该链表的。
- empty:这个链表里的span,所有的span都不确定里面是否有空闲的对象空间。当一个span交给mcache的时候,就会加入到empty链表。
mcentral会先从nonempty搜索满足条件的span,如果没有找到再从emtpy搜索满足条件的span,然后把找到的span交给mcache。
mcentral需要向mheap提供需要的内存页数和span class级别,然后它优先从free中搜索可用的span,如果没有找到,会从scav中搜索可用的span,如果还没有找到,它会向OS申请内存,再重新搜索2棵树,必然能找到span。如果找到的span比需求的span大,则把span进行分割成2个span,其中1个刚好是需求大小,把剩下的span再加入到free中去,然后设置需求span的基本信息,然后交给mcentral。
大对象:
99%的流程与mcentral向mheap申请内存的相同
栈内存:每个goroutine都有自己的栈,栈的初始大小是2KB,100万的goroutine会占用2G,但goroutine的栈会在2KB不够用时自动扩容,当扩容为4KB的时候,百万goroutine会占用4GB。在rpc调用(grpc invoke)时,栈会发生扩容(runtime.morestack),也就意味着在读写routine内的任何rpc调用都会导致栈扩容, 占用的内存空间会扩大为原来的两倍,4kB的栈会变为8kB,100w的连接的内存占用会从8G扩大为16G(全双工,不考虑其他开销)
- 根据分配对象的大小,选用不同的结构做分配。包括 3 种情况:
- 小于 16B 的用 mcache 中的 tiny 分配器分配;
- 大于 32KB 的对象直接使用堆区分配;
- 16B 和 32KB 之间的对象用 mspan 分配。
- 现在我们假定分配对象大小在 16B 和 32KB 之间。在 mcache 中找到合适的 mspan 结构,如果找到了就直接用它给对象分配内存。
- 我们这里假定此时没有在 mcache 中找到合适的 mspan。需要到 mcentral 结构中查找到一个 mspan 结构并返回。虽然 mcentral 结构对 mspan 的大小和是否空闲进行了分类管理,但是它对所有的 P 都是共享的,所以每个 P 访问 mcentral 结构都要加锁。
- 假定 Go 运行时在进行了一些扫描回收操作之后,在 mcentral 结构还是没有找到合适的 mspan。Go 运行时就会建立一个新的 mspan,并找到 heapArea 分配相应的页面,把页面地址和数量写入 mspan 中。然后,把 mspan 插入 mcentral 结构中,返回的同时将 mspan 插入 mcache 中。最后用这个新的 mspan 分配对象,返回对象地址。
微小对象分配:
从tiny block中分配,这些对象被打包到线程本地缓存(mcache)中,在同一个内存块中存储多个 tiny 对象,从而减少碎片化和全局锁的争用。