内存池——TCMalloc&JEMalloc
在应用层业务代码与内核之间,一般有两层内存池:应用层内存池和C库内存池。
当代码申请内存时,首先会到达应用层内存池,如果应用层内存池有足够的可用内存,就会直接返回给业务代码,否则,它会向更底层的 C 库内存池申请内存。比如,如果我们在Apache、Nginx 等服务之上做模块开发,这些服务中就有独立的内存池。
C库内存池主要有Google 的 TCMalloc 和 FaceBook 的 JEMalloc,当C库内存池无法满足内存申请时,才会向操作系统申请分配内存。
Linux 系统默认的C库内存池为 Ptmalloc2,C 库内存池工作时,会预分配比你申请的字节数更大的空间作为内存池。比如说,当主进程下申请 1 字节的内存时,Ptmalloc2 会预分配 132K 字节的内存,应用代码再申请内存时,会从这已经申请到的 132KB 中继续分配。
当我们释放这 1 字节时,Ptmalloc2 也不会把内存归还给操作系统,而是先缓存着放进内存池里,进程再次申请内存时就可以直接复用。
当然多线程与单线程的预分配策略不同,每个子线程预分配的内存是64MB,(Ptmalloc2 中被称为 Thread Arena,32 位系统下为1MB,64 位系统下为 64MB)如果有100个线程,就将有6GB的内存都会被内存池占用。当然,子线程内存池最多只能到8倍的CPU核数。
如果我们不想使用这预分配出来的6GB内存应该怎么办?有两种方法:
- 调整Ptmalloc2的工作方式,通过设置MALLOC_ARENA_MAX 环境变量,可以限制线程内存池的最大数量。当然,线程内存池的数量减少后,会影响 Ptmalloc2 分配内存的速度
- 更换Ptmalloc2 内存池,选择一个预分配内存更少的内存池,比如 Google 的TCMalloc
Ptmalloc2 和TCMalloc 区别
上面也说了,TCMalloc 主要的特性是预分配内存更少,对多线程下小内存的分配特别友好。
比如,在 2GHz 的 CPU 上分配、释放 256K 字节的内存,Ptmalloc2 耗时 32 纳秒,而TCMalloc 仅耗时 10 纳秒,差距超过了 3 倍,这是因为,Ptmalloc2 假定,如果线程 A 申请并释放了的内存,线程 B 可能也会申请类似的内存,所以它允许内存池在线程间复用以提升性能。
Ptmalloc2 需要加锁是造成性能差异的主要原因。每次分配内存,Ptmalloc2 一定要加锁,才能解决共享资源的互斥问题。然而加锁的消耗并不小。如果你监控分配速度的话,会发现单线程服务调整为 100 个线程,Ptmalloc2 申请内存的速度会变慢 10 倍。TCMalloc 针对小内存做了很多优化,每个线程独立分配内存无须加锁,所以速度更快。而且线程数越多,Ptmalloc2 出现锁竞争的概率就越高。比如我们用 40 个线程做同样的测试,TCMalloc 只是从 10 纳秒上升到 25 纳秒,只增长了 1.5 倍,而 Ptmalloc2 则从32 纳秒上升到 137 纳秒,增长了 3 倍以上。因此当应用场景涉及大量的并发线程时,换成TCMalloc库也更有优势。
而GlibC将Ptmalloc2作为默认内存池也是有原因的,因为Ptmalloc2更擅长大内存的分配。
比如,单线程下分配 257K 字节的内存,Ptmalloc2 的耗时不变仍然是 32 纳秒,但TCMalloc 就由 10 纳秒上升到 64 纳秒,增长了 5 倍以上。这是因为 TCMalloc 特意针对小内存做了优化。
那么多少字节叫小内存呢?TCMalloc 把内存分为 3 个档次,小于等于 256KB 的称为小内存,从 256KB 到 1M 称为中等内存,大于 1MB 的叫做大内存。TCMalloc 对中等内存、大内存的分配速度很慢,比如我们用单线程分配 2M 的内存,Ptmalloc2 耗时仍然稳定在 32 纳秒,但 TCMalloc 已经上升到 86 纳秒,增长了 7 倍以上。
所以,如果主要分配 256KB 以下的小内存,特别是在多线程环境下,应当选择 TCMalloc;否则应使用Ptmalloc2,它的通用性更好。
从堆还是栈上分配内存?
上面讨论的内存池都是堆内存,如果我们把在堆中分配的对象改为在栈上分配,那么速度还会再快一倍。
这是因为,由于每个线程都有独立的栈,所以分配内存时不需要加锁保护,而且栈上对象的尺寸在编译阶段就已经写入可执行文件了,执行效率更高!
当然在栈中分配内存的缺点也很明显:1)栈内存生命周期有限,它会随着函数调用结束后自动释放。2)栈的容量有限,如CentOS7 中是8MB字节,如果你申请的内存超过限制会造成栈溢出错误。(递归函数很容易造成这类问题),而堆则没有容量限制。
因此在满足功能的情况下, 我们可以选择在栈中分配内存。