[golang]内存管理(涉及tcmalloc,gc等)

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也是要加锁的。

  1. 线程私有性:ThreadCache,顾名思义,是每个线程一份的。理想情况下,每个线程的内存需求都在自己的ThreadCache里面完成,线程之间不需要竞争,非常高效。而CentralCache和PageHeap则是全局的;
  2. 内存分配粒度:在tcmalloc里面,有两种粒度的内存,object和span。span是连续page的内存,而object则是由span切成的小块。object的尺寸被预设了一些规格(class),比如16字节、32字节、等等,同一个span切出来的object都是相同的规格。object不大于256K,超大的内存将直接分配span来使用。ThreadCache和CentralCache都是管理object,而PageHeap管理的是span。

 

alloc


  1. 根据分配size,判断是小块内存还是大块内存(256K为界)
  2. 小块内存:
    1. 通过size得到对应的class;
    2. 先尝试在ThreadCache.list_[class]的FreeList里面分配,分配成功则直接返回;
    3. 尝试在CentralCache里面分配batch_size个object,其中一个用于返回,其他的都加进ThreadCache.list_[class];
    4. 拿到class对应的CentralFreeList;
    5. 尝试在CentralFreeList.tc_slots_[]里面分配(CentralFreeList.used_slots_是空闲slot游标);
    6. 尝试在CentralFreeList.nonempty_里面分配,尽量分配batch_size个object。但最后只要分配了多于一个object,即可返回;
    7. 如果CentralFreeList.nonempty_为空,则要向PageHeap去申请一个span。对应的class申请的span应该包含多少个连续page,这个也是预设好的。拿到span之后将其拆分成N个object,然后返回前面所需要的object;
      1. PageHeap先从伙伴系统对应npages的span链表里面查找空闲的span(优先查normal链、然后returned链),有则直接返回;
      2. 在更大npages的span里面查找空闲的span(优先查normal链、然后returned链),有则将其拆小,并返回所需要的span;
      3. 向kernel申请若干个page的内存(可能大于npage),返回所需要的span,其他的span放回伙伴系统;
  3. 大块内存:
    1. 直接向PageHeap去申请一个刚好大于等于请求size的span。申请过程与小块内存走到这一步时的过程一致;

这个图真心很清晰,大佬讲的很好

 

free


  1. 通过释放的ptr,在PageHeap维护的映射关系中,找到对应span的class(先尝试在cache里面找,没有再查radix tree,然后插入cache。cache里面自动淘汰老的项)。class为0代表ptr指向的是大块内存;
  2. 小块内存:
    1. 将ptr指向的内存释放到ThreadCache.list_[class]里面;
    2. 如果ThreadCache.list_[class]长度超过阈值(FreeList.length_>=FreeList.max_length_),或者ThreadCache的容量超过阈值(ThreadCache.size_>=ThreadCache.max_size_),则触发回收过程。两种情况分别针对class对应的FreeList,和ThreadCache下面的所有FreeList进行回收(具体的策略后续再讨论);
    3. object被回收到CentralCache里面class对应的CentralFreeList上。先尝试batch_size个object的整块回收,CentralFreeList会试图将其释放到自己的cache里面去(tc_slots_);
    4. 如果cache装满,或者凑不满batch_size个整数的object,则单个回收,回收进其对应的span.objects。这个回收过程不必拿着object在CentralFreeList的span链表中逐个去寻找自己对应的span,而是通过PageHeap中的对应关系直接找到span;
    5. 如果span下面的object都已经回收了(refcount_减为0),则进一步将其释放回PageHeap;
      1. 在radix tree中找到span之前和这后的span,如果它们空闲且也在normal链上,则进行合并;
      2. PageHeap将多余的span回收到其对应的returned链上,然后继续考虑span之间的合并(要求span都在returned链上)(具体策略后续再讨论);
  3. 大块内存:
    1. 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数量进行排序:

  1. free:free中保存的span是空闲并且非垃圾回收的span。
  2. 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  这篇写的最清楚

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值