操作系统内存分配算法_Go 内存管理(二): TCMalloc分配算法

和大家分享一篇关于Go内存管理的文章,下文笔者并不会完全按照原文进行翻译,而是加了一些自己的见解和其他出处的内容,有错漏欢迎指出。
https://zhuanlan.zhihu.com/p/297665603​zhuanlan.zhihu.com
zhihu-card-default.svg
上一篇文章我们回顾了操作系统和内存管理的基本概念,这里我们来介绍Go使用的内存分配算法 TCMalloc。

Go没有使用​malloc​接口来获取内存,而是通过​mmap​直接向操作系统申请,因此它需要自己实现内存的分配和回收。Go的内存分配器是基于TCMalloc(Thread-Caching Malloc)来实现的。简单来说,TCMalloc将内存空间分成了不同尺寸大小的块,申请内存时会分配一个合适大小的内存块,这些空闲块通过链表维护。有些尺寸小的块是线程自主使用,不够用的时候才会从共有块中申请。TCMalloc的特点在于:

  • 比glibc 2.3 malloc更快,后者有个叫做​ptmalloc2​独立库,可以在300ns左右的时间完成一次malloc。而TCMalloc只需要大概50ns。
  • 对于多线程的程序,TCMalloc也可以减少锁冲突。对于小对象,可以认为没有冲突。对于大对象,TCMalloc会使用适合粒度和高效的spinlock(循环请求)。

TCMalloc

注意这一节里讨论的对象 object是指用于分配的一块内存,不是指应用程序里面常说的对象。

TCMalloc的性能这么好的原因是它使用线程本地缓存(thread-local cache)来存储预分配的内存对象,所以一些小对象可以直接分配到线程本地缓存。有需要的适合,中心数据结构会被移动到线程缓存,而定时gc又会从线程缓存迁移内存到中心数据结构。

9ac9bc8cc50b4703614a0b2d67f9e10e.png


在TCMalloc的概念里,小于等于32K的对象称之为小对象,小对象和大对象的处理方式不一样。大对象会从中心heap上直接申请内存,单位是页(4k大小),因此大对象占用的大小是若干页,即使实际上不需要这么多内存。

小对象分配

每个小对象按照大小会被映射到大概170个不同大小的类型。比如,961到1024字节之间的对象分配时会向上找到最接近近的大小,即1024字节。这些类型都是由8字节、16字节、32字节这些小块组成,最大的小块是256字节。线程缓存对于每种大小类型都有一条单向链表,存储了该大小的空闲对象。

cbc50428183f63df42783428403cb5c8.png
线程缓存

80374f9dd867c7892a839d89a58a9d49.png
小对象分配流程图

大对象分配

大对象会在中心页heap中分配,分配时以页为单位。这个堆也是空闲对象的列表数组。对于k<256,k page意味着每个对象都是连续的k页。在查找k页大小的对象时,会从小到达查找直到第一个空闲的对象。假如都为空,则从操作系统申请内存。假如最终分配的对象比需要的k页大,剩余的页会重新插入其他适合的空闲链表。

eba346db42489be0b3956b8a2fe877cf.png

Span

TCMalloc通过一系列页来管理heap。连续的若干页用一个span对象表示。span对象可以是已分配或空闲。空闲时,span是heap页链表中的一项。如果是已分配,span可能是应用使用的一个大对象,也可能是划分成多个小对象的连续页。后者的情况下,span会记录下对象的尺寸类型(size-class)。一个使用页序号检索的中心化数组能够用于查找一页所属的span。

e9384c740bfd0e2d0cda700fd277b0b8.png

如何释放对象

当一个对象不再被使用时,我们可以算出它的页序号,进而从中心化数组中找到对应的span。这个span可以告诉我们这是一个大对象,还是小对象。

  • 如果是一个小对象,它就会被插入到当前线程的线程缓存中对应尺寸的空闲链表。如果线程缓存的空间超过一个预先设置的阈值(默认是2MB),我们就会运行gc来将线程缓存中未使用的对象移回中心化空闲链表。
    • 一条中心化空闲链表代表一种尺寸大小的对象,由span集合组成,每个span又包含了一条空闲对象的链表。分配出去的对象被释放时又会回归这个span。如果一个span包含的所有对象都变成空闲,那么这个span会整个被释放到page heap。
    • 每次gc时计算需要移动的对象个数算法如下:对于一条链条,记录从上次gc到现在它的最小长度L,移动的个数则为L/2。这种做法的一个好处是当一个线程不需要某个尺寸的对象时,可以快速清空这条链表,以让其他线程使用。
  • 如果是一个大对象,span能够告诉我们这个对象覆盖的页范围[p,q]。我们分别找出p-1,q+1页所在的span,假如这两个span其中之一是空闲的,我们可以将它与[p,q]合并,合并后的span会被插入到适合大小的空闲链表。

TMalloc的优缺点

通过对比不同数量的thread下TMalloc和PTMalloc2的性能(分配不同大小内存时分配和回收的操作频率高低)。总的来说,TCMalloc更加稳定,更快。由于gc阈值的限制,随着内存大小的上升,gc会导致越来越少对象能够保留在线程缓存中,性能会有较大的下降。由于线程缓存的最大尺寸是32k,因为尺寸大于32k后,这部分内存会直接从page heap分配,所以尺寸再大性能下降也不大了。下面只摘出两副对比图。

6a81641cb702cb523e687ca4bde1f944.png

a8ff23081a1b2350b4c7f760b227a45f.png

但是TCMalloc也不是万能的。对于不是使用libpthread.so链接的应用,TCMalloc不是一个很好的选择。相比其他的分配算法,TCMalloc有时会更容易陷入内存饥荒。另一个问题是当前的TCMalloc不会返回内存给操作系统。对于一个正在运行二进制程序,不要加载TCMalloc,这样会导致通过系统malloc的内容被释放到TCMalloc中,TCMalloc无法处理这些对象。

Reference

TCMalloc : Thread-Caching Malloc​goog-perftools.sourceforge.net
34741c58e510269b9a5c64c5f7ce7a5e.png
https://povilasv.me/go-memory-management/#​povilasv.me
ed667a155d31fb1c0457f1634a5be117.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值