和大家分享一篇关于Go内存管理的文章,下文笔者并不会完全按照原文进行翻译,而是加了一些自己的见解和其他出处的内容,有错漏欢迎指出。https://zhuanlan.zhihu.com/p/297665603zhuanlan.zhihu.com
上一篇文章我们回顾了操作系统和内存管理的基本概念,这里我们来介绍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又会从线程缓存迁移内存到中心数据结构。
在TCMalloc的概念里,小于等于32K的对象称之为小对象,小对象和大对象的处理方式不一样。大对象会从中心heap上直接申请内存,单位是页(4k大小),因此大对象占用的大小是若干页,即使实际上不需要这么多内存。
小对象分配
每个小对象按照大小会被映射到大概170个不同大小的类型。比如,961到1024字节之间的对象分配时会向上找到最接近近的大小,即1024字节。这些类型都是由8字节、16字节、32字节这些小块组成,最大的小块是256字节。线程缓存对于每种大小类型都有一条单向链表,存储了该大小的空闲对象。
大对象分配
大对象会在中心页heap中分配,分配时以页为单位。这个堆也是空闲对象的列表数组。对于k<256,k page意味着每个对象都是连续的k页。在查找k页大小的对象时,会从小到达查找直到第一个空闲的对象。假如都为空,则从操作系统申请内存。假如最终分配的对象比需要的k页大,剩余的页会重新插入其他适合的空闲链表。
Span
TCMalloc通过一系列页来管理heap。连续的若干页用一个span对象表示。span对象可以是已分配或空闲。空闲时,span是heap页链表中的一项。如果是已分配,span可能是应用使用的一个大对象,也可能是划分成多个小对象的连续页。后者的情况下,span会记录下对象的尺寸类型(size-class)。一个使用页序号检索的中心化数组能够用于查找一页所属的span。
如何释放对象
当一个对象不再被使用时,我们可以算出它的页序号,进而从中心化数组中找到对应的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分配,所以尺寸再大性能下降也不大了。下面只摘出两副对比图。
但是TCMalloc也不是万能的。对于不是使用libpthread.so链接的应用,TCMalloc不是一个很好的选择。相比其他的分配算法,TCMalloc有时会更容易陷入内存饥荒。另一个问题是当前的TCMalloc不会返回内存给操作系统。对于一个正在运行二进制程序,不要加载TCMalloc,这样会导致通过系统malloc的内容被释放到TCMalloc中,TCMalloc无法处理这些对象。