传统的内存分配和现代的内存分配
传统的内存分配是在需要内存的时候使用 malloc()
函数直接向操作系统申请内存,在释放内存的时候用 free()
把内存还给操作系统。
malloc = memory allocate
直接使用这两个函数来管理内存的问题在于,每次申请内存都是一个很耗时的操作,而且频繁申请和释放内存会导致内存有很多碎片(外部碎片)。
外部碎片:分散在内存中的小块内存,其总和可以满足某个进程的申请要求,但由于内存碎片不连续,进程无法一次性申请这些内存。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
| +-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+-----------------------------------------------------+
| |-------| | |-------| | |-------------| | |
| |-------| | |-------| | |-------------| | |
+-----------+-----------+-----------------+-----+-----+
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+-----------------+-----------------------------------+
| |-------| | | |-------------| | |
| |-------| | | |-------------| | |
+-----------+-----------+-----------------+-----+-----+
|
- 申请 2 块单位为 2 的内存和 1 块单位为 3 的内存;
- 释放掉中间一块单位为 2 的内存;
- 此时如果要申请一块长度为 3 的内存,无法申请到。
为了解决这些问题,现代的内存分配器会一次性向操作系统申请一块(或者一堆)大内存。大内存可按照特定单位切割成一块块大小相等的内存块,不同类型的单位切割成的内存块大小不同。当进程内部需要内存的时候,找到一块与所需内存大小非常接近但又比所需内存大的内存块,并直接使用。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 内存以 3 为单位做切割。
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+-----------+-----------------+-----------------------+
| |--------| | |-------| | |-------------| |
| |--------| | |-------| | |-------------| |
+-----------------+-----------------+-----------------+
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+-----------+-----------+-----+-----------------------+
| |--------| | | |-------------| |
| |--------| | | |-------------| |
+-----------------+-----------------+-----------------+
|
- 申请 2 块单位为 2 的内存和 1 块单位为 3 的内存;
- 释放掉中间一块单位为 2 的内存;
- 此时如果要申请一块长度为 3 的内存,可以直接申请中间那块。
这比向系统直接申请的方式快得多,并且减少了外部碎片。但因为内存块往往比所需内存大一点,多出来的部分就成了内部碎片。这个问题相比外部碎片好一些,内部碎片可以通过设置以不同单位为块的内存来缓解。
现代内存分配器
现代内存分配器例如 Linux 的 SLUB Allocator (前身是 SLAB Allocator),此外还有比较常见的: jemalloc 、 TCMalloc 和 ptmalloc 。
TCMalloc = Thread-Caching Malloc
ptmalloc = pthreads Malloc
以下举一些例子。
PHP
PHP 的内存分配器参考 jemalloc 和 TCMalloc 的实现。
Zend/zend_alloc.c
:
1
2
3
4
5
| /*
* zend_alloc is designed to be a modern CPU cache friendly memory manager
* for PHP. Most ideas are taken from jemalloc and tcmalloc implementations.
* ....
*/
|
Golang
Golang 的内存分配器是基于 TCMalloc 实现的。
runtime/malloc.go
:
1
2
3
| // Memory allocator.
//
// This was originally based on tcmalloc, but has diverged quite a bit.
|
Redis
Redis 把 jemalloc 和 TCMalloc 作为可选项,可以在编译的时候选择用哪种分配器。
src/zmalloc.h
:
1
2
3
4
5
6
7
8
9
10
11
| #if defined(USE_TCMALLOC)
// 省略
#include <google/tcmalloc.h>
// 省略
#endif
#elif defined(USE_JEMALLOC)
// 省略
#include <jemalloc/jemalloc.h>
// 省略
#endif
|
在 redis 源码中,自带了 jemalloc 源码 deps/jemalloc
。
Memcache
使用的是 SLAB Allocator 的机制。
1
2
3
4
| /*
* Slabs memory allocation, based on powers-of-N.
* ....
*/
|
扩展阅读
内存优化总结:ptmalloc、tcmalloc和jemalloc: http://www.cnhalo.net/2016/06/13/memory-optimize/
图解Go内存分配器: https://tonybai.com/2020/02/20/a-visual-guide-to-golang-memory-allocator-from-ground-up/
Memcache-内存模型-源码分析: https://www.jianshu.com/p/a824ae00d9bb
Linux slab 分配器剖析: https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/index.html
图解 TCMalloc: https://zhuanlan.zhihu.com/p/29216091
(七)PHP内存管理详解: https://blog.csdn.net/IT_10/article/details/94768679