1.操作系统存储模型
不赘述了,大学都学过.
cpu寄存器-->L1/L2/L3缓存-->内存-->磁盘
物理内存和虚拟内存.
我们程序申请的内存都是虚拟内存,虚拟内存和物理内存通过页表进行关联. 而实际内存的分配也不是你申请了虚拟内存就会分配物理内存的,而是真正要用的时候,发送缺页异常,才会分配.
虚拟内存的布局
用户空间和内核空间
内核空间:每个进程都有内核空间,但是他们映射的同一个物理段.内核空间虽然用户进程也知道,但是用不了,因为那是老大的空间,小弟不能乱用
用户空间:每个进程独有.下面是用户空间的分布
Text Segment | 存放二进制可执行代码 |
Data Segment | 存放静态常量 |
BSS Segment | 存放未初始化的静态变量 |
Heap | 动态分配(malloc),向上分配 |
Stack | 向下分配 主线程函数调用使用的这里的栈空间 而主线程之外的线程,使用的函数栈,是进程的堆空间里分配的一段内存作为函数栈使用 |
怎么对堆进行管理?
堆分配的时候,可以调用malloc,malloc会把brk向上移动,那么移动出来的区域就是新内存了.具体怎么分配可以参考 malloc/brk/sbrk
而堆一般都是怎么规划呢?堆就是一个大块的0101.当被申请使用的时候,我们需要记录下哪一块被申请使用了,哪一块没被使用.因此最简单的如图.
把堆变成一个链.每个节点都是一个内存对象. 分为4个部分
1.size 这个内存块的大小
2.used 标志是否被使用
3.next 指针,下一个块
4.data 数据块
而为了实现缓存对齐,应该还要加一些冗余字段,你要5个字节,我会多给你3个,为什么要缓存对齐呢?其实在各个底层的代码里,都有,为了防止false sharing.
注意:
虚拟内存地址是一个进程公用的,如果一个进程有多个线程,那么在申请内存的时候,势必会造成竞争,如果每次都加锁,性能将大大降低.怎么解决多线程内存的分配呢?
2.TcMalloc
明确几个概念
1.Page:操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。《TCMalloc解密》里称x64下Page大小是8KB。
2.Span:一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
3.ThreadCache:每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块(我们把他叫做object吧),同一个链表上object的大小是相同的,也可以说按object大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
4.CentralCache:并不像ThreadCache那样直接维护object的链表,而是维护span的链表,每个span下面再挂一个由这个span切分出来的object的链.这里维护的其实也是object了.当threadCache没有object的话,就会来CentralCache中拿.最开始的时候,CentralCache中只有256page的span.如果你想要一个1page的span.那么就会把这个大的span切分成1+255的两个span,把1page span返回给ThreadCache.
当object使用完,归还时,会优先放到tc_slots_[class]这个缓存,当然取的时候也是这样.
5.PageHeap:PageHeap是堆内存的抽象,PageHeap存的是Span的链表,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。PageHeap也是要加锁的。
- 线程私有性:ThreadCache,顾名思义,是每个线程一份的。理想情况下,每个线程的内存需求都在自己的ThreadCache里面完成,线程之间不需要竞争,非常高效。而CentralCache和PageHeap则是全局的;
- 内存分配粒度:在tcmalloc里面,有两种粒度的内存,object和span。span是连续page的内存,而object则是由span切成的小块。object的尺寸被预设了一些规格(class),比如16字节、32字节、等等,同一个span切出来的object都是相同的规格。object不大于256K,超大的内存将直接分配span来使用。ThreadCache和CentralCache都是管理object,而PageHeap管理的是span。
alloc
- 根据分配size,判断是小块内存还是大块内存(256K为界)
- 小块内存:
- 通过size得到对应的class;
- 先尝试在ThreadCache.list_[class]的FreeList里面分配,分配成功则直接返回;
- 尝试在CentralCache里面分配batch_size个object,其中一个用于返回,其他的都加进ThreadCache.list_[class];
- 拿到class对应的CentralFreeList;
- 尝试在CentralFreeList.tc_slots_[]里面分配(CentralFreeList.used_slots_是空闲slot游标);
- 尝试在CentralFreeList.nonempty_里面分配,尽量分配batch_size个object。但最后只要分配了多于一个object,即可返回;
- 如果CentralFreeList.nonempty_为空,则要向PageHeap去申请一个span。对应的class申请的span应该包含多少个连续page,这个也是预设好的。拿到span之后将其拆分成N个object,然后返回前面所需要的object;
- PageHeap先从伙伴系统对应npages的span链表里面查找空闲的span(优先查normal链、然后returned链),有则直接返回;
- 在更大npages的span里面查找空闲的span(优先查normal链、然后returned链),有则将其拆小,并返回所需要的span;
- 向kernel申请若干个page的内存(可能大于npage),返回所需要的span,其他的span放回伙伴系统;
- 大块内存:
- 直接向PageHeap去申请一个刚好大于等于请求size的span。申请过程与小块内存走到这一步时的过程一致;
这个图真心很清晰,大佬讲的很好
free
- 通过释放的ptr,在PageHeap维护的映射关系中,找到对应span的class(先尝试在cache里面找,没有再查radix tree,然后插入cache。cache里面自动淘汰老的项)。class为0代表ptr指向的是大块内存;
- 小块内存:
- 将ptr指向的内存释放到ThreadCache.list_[class]里面;
- 如果ThreadCache.list_[class]长度超过阈值(FreeList.length_>=FreeList.max_length_),或者ThreadCache的容量超过阈值(ThreadCache.size_>=ThreadCache.max_size_),则触发回收过程。两种情况分别针对class对应的FreeList,和ThreadCache下面的所有FreeList进行回收(具体的策略后续再讨论);
- object被回收到CentralCache里面class对应的CentralFreeList上。先尝试batch_size个object的整块回收,CentralFreeList会试图将其释放到自己的cache里面去(tc_slots_);
- 如果cache装满,或者凑不满batch_size个整数的object,则单个回收,回收进其对应的span.objects。这个回收过程不必拿着object在CentralFreeList的span链表中逐个去寻找自己对应的span,而是通过PageHeap中的对应关系直接找到span;
- 如果span下面的object都已经回收了(refcount_减为0),则进一步将其释放回PageHeap;
- 在radix tree中找到span之前和这后的span,如果它们空闲且也在normal链上,则进行合并;
- PageHeap将多余的span回收到其对应的returned链上,然后继续考虑span之间的合并(要求span都在returned链上)(具体策略后续再讨论);
- 大块内存:
- ptr对应的直接就是一个span,直接将其释放回PageHeap即可
自己总结下:
小对象要会受到ThreadCache中,ThreadCache中的对象链表如果满了,就去归还给CentralCache.归还CentralCache时优先批量回收到list,如果不到批量那么多久归还到tc_slots中.
3.Go内存管理
其实与tcMalloc类似.不同点在于
1.TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcache,因为在Go程序中,当前最多有GOMAXPROCS个线程在用户态运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问,线程的运行又是与P绑定的,把mcache交给P刚刚好。
2.MCenter与tcmalloc类似
3.mheap与TCMalloc中的PageHeap类似
mheap里保存了2棵二叉排序树,按span的page数量进行排序:
free
:free中保存的span是空闲并且非垃圾回收的span。scav
:scav中保存的是空闲并且已经垃圾回收的span。
大体流程
参考
https://blog.csdn.net/gfgdsg/article/details/42709943 malloc/brk/mmap
https://mp.weixin.qq.com/s/3gGbJaeuvx4klqcv34hmmw
https://zhuanlan.zhihu.com/p/29216091
https://developer.aliyun.com/article/6045 这篇写的最清楚