前言
zend_alloc
是 PHP 最关键的内存管理工具,本文将首先介绍zend_alloc
的整体架构与原理,为深入理解 zend_alloc
的实现奠定基础,然后在后续章节中逐步展开相关文件、主要功能、数据结构以及初始化流程。
注:本文所指"用户态内存",是指 PHP 进程在用户空间中由 Zend 引擎自行管理的堆内存,用于脚本执行时的变量、结构体、字符串等数据,不包含内核态或共享内存区域。
新的内存管理器(从 PHP 5.2 开始使用)会从整体上减少内存分配,并提升内存分配速度。
zend_alloc
是为 PHP 设计的、对现代 CPU 缓存友好的内存管理工具。大部分构思来源于 jemalloc
和 tcmalloc
的实现。
所有的内存分配被划分成三种:这些分类依据的是分配块的大小,每种类型对应不同的分配策略。
-
巨大块(huge block) :尺寸比 chunk(默认2MB)更大,使用
mmap()
函数来进行分配。分配结果在内存中对齐到2MB(大小是2MB的倍数)。 -
大块(large block) :每个 chunk 包含一串4KB大小的 page。大块的内存块总是对齐到 page 边界(向上取整到 page 的倍数)。大块分配用于3KB至2MB之间的内存请求,以整页方式对齐分配。
-
小块(small block) :小于 page 大小的3/4(3KB)。小块尺寸向上取最接近的预定义数字的值(在
ZEND_MM_BINS_INFO
中共有30个预定义的数字:8, 16, 24, 32, ... 3072)。小块内存是通过 run 分配的。每个 run 被分配成一个单独的或一些连续的 page。每个 run 里的空间被分配成一串空间的元素链表,占用的总空间是8B的倍数。
zend_alloc
通过 chunk 从操作系统中分配内存。这些 chunk 和巨大的内存块总是与 chunk 对齐(大小是 chunk 的倍数)。因此,PHP 可以通过位运算快速确定任意指针属于哪个 chunk。
普通 chunk 的开头保留一个特殊用途的单独 page,用于存放:
-
空闲 page 的 bitset
-
预定义的小尺寸有效运行的 bitset
-
保存 chunk 中每个 page 使用信息的 page 映射表
zend_alloc
提供类似 emalloc
/ efree
/ erealloc
的 API,但它还提供了专用的和优化的方法来分配预定义大小的内存块(例如 emalloc_2()
、emalloc_4()
、...、emalloc_large()
等)。当需要的内存大小已知时,这个类库使用 C 语言预处理器(preprocessor)技术和更多的专用方法来代替调用 emalloc()
函数。
来源:zend_alloc.c
本篇文章旨在介绍 zend_alloc
的实现逻辑和原理,源码版本为 8.2.5。
本篇文章偏向底层源码解析,适合对 PHP 内核、内存管理、C 语言感兴趣的读者。如果你只想快速了解 PHP 是如何管理内存的,可以重点阅读"小结"和"结构关系图"部分。
一、相关文件
与 zend_alloc
内存管理相关的文件主要有以下三个:
文件名 | 说明 |
---|---|
zend_alloc.h |
C 语言头文件,定义接口与结构体 |
zend_alloc.c |
核心业务逻辑实现 |
zend_alloc_sizes.h |
存放常用常量和宏定义 |
这三个文件构成了 PHP 内存管理器的完整实现:定义接口、实现逻辑与配置常量。
关键常量
以下是 zend_alloc_sizes.h
文件中定义的关键常量:
arduino
#define ZEND_MM_CHUNK_SIZE ((size_t) (2 * 1024 * 1024)) // chunk 大小 2MB
#define ZEND_MM_PAGE_SIZE (4 * 1024) // 每个 page 4KB
#define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) // 一个 chunk 可分成512个 page
#define ZEND_MM_FIRST_PAGE (1) // 第一个 page 编号1
#define ZEND_MM_MIN_SMALL_SIZE 8 // 小块最小 8B
#define ZEND_MM_MAX_SMALL_SIZE 3072 // 小块最大 3072B
#define ZEND_MM_MAX_LARGE_SIZE (ZEND_MM_CHUNK_SIZE - (ZEND_MM_PAGE_SIZE * ZEND_MM_FIRST_PAGE)) // 大块内存最大2MB - 4KB = 2044KB
常量名 | 值 | 说明 |
ZEND_MM_CHUNK_SIZE |
2MB | PHP 向操作系统申请的最小内存单元 |
ZEND_MM_PAGE_SIZE |
4KB | chunk 内的最小逻辑页大小 |
ZEND_MM_PAGES |
512 | 每个 chunk 包含512页 |
ZEND_MM_FIRST_PAGE |
1 | 第0页为管理页,从第1页开始分配 |
ZEND_MM_MIN_SMALL_SIZE |
8B | 小块最小分配单位(8 字节对齐) |
ZEND_MM_MAX_SMALL_SIZE |
3072B | 小块最大值(page 的3/4) |
ZEND_MM_MAX_LARGE_SIZE |
2044KB | 大块分配上限(去除管理页) |
这些常量定义了 PHP 内存分配的几何结构,是后续所有 chunk、page、run 计算的基础。
二、主要功能
了解内存管理器的主要功能,可以从 zend_alloc.h
中的标准封装宏程序(Standard wrapper macros)列表开始。这些封装宏的设计目的在于统一内存管理接口,便于调试与内存统计,从而在整个 Zend 引擎中提供一致的内存操作入口。以下列出了 Zend 内存分配器对外提供的标准 API 及其底层实现关系:
功能分组 | 宏名称 | 目标方法 | 说明 |
分配内存 | emalloc() | _emalloc() | 通用方法,外部大量调用 |
emalloc_large() | _emalloc_large() | 分配大块内存,内部少量调用 | |
emalloc_huge() | _emalloc_huge() | 分配巨大块内存,内部少量调用 | |
safe_emalloc() | _safe_emalloc() | 分配内存,带安全保护,外部大量调用 | |
ecalloc() | _ecalloc() | 分配内存并清零,外部大量调用 | |
释放内存 | efree() | _efree() | 通用方法,外部大量调用 |
efree_large() | _efree_large() | 释放大块内存,内部少量调用 | |
efree_huge() | _efree_huge() | 释放巨大块内存,内部少量调用 | |
调整内存大小 | erealloc() | _erealloc() | 调整内存大小,外部大量调用 |
erealloc2() | _erealloc2() | 调整内存大小,内部少量调用 | |
safe_erealloc() | _safe_erealloc() | 调整内存大小,带安全保护,外部大量调用 | |
创建内存并复制指定内容 | estrdup() | _estrdup() | 使用 _emalloc() 函数创建并复制指定字符串(不传入长度) |
estrndup() | _estrndup() | 使用 _emalloc() 函数创建并复制指定字符串(传入长度) |
|
zend_strndup() | _zend_strndup() | 使用原生函数创建并复制指定字符串(传入长度) |
可见,内存管理器最重要的功能是分配内存、释放内存和调整内存大小。这些接口构成了 Zend 引擎内存管理的外部 API 层,是所有扩展模块和核心组件的基础。下面以这几项主要功能为线索,依次展开介绍。
三、基本数据结构
为了支撑上节所述的三种内存分配方式,zend_alloc
定义了一系列核心数据结构,用于记录堆(heap)、块(chunk)、页(page)及运行状态。内存分配中用到的主要结构如下:
名称 | 结构体 | 别名 | 备注 |
- | _zend_alloc_globals | zend_alloc_globals | 用作全局入口 |
heap | _zend_mm_heap | zend_mm_heap | 堆结构,是整个内存系统的根节点 |
chunk | _zend_mm_chunk | zend_mm_chunk | 块,分配单元,管理页集合 |
page | _zend_mm_page | zend_mm_page | 页,最小逻辑内存单位(4KB) |
zend_alloc_globals
结构体在 zend_alloc.c
中声明。它在全局只有一个实例,充当全局入口,指向当前进程的 zend_mm_heap
对象。它的结构非常简单,仅包含一个指向 zend_mm_heap
实例的指针。
zend_mm_heap
结构体是整个内存的根,所有 chunk 都与它关联。它除了保留主 chunk 的指针,还要记录内存使用情况,主要结构如下:
元素名 | 类型 | 说明 |
free_slot | zend_mm_free_slot[] | 空闲小块内存链表指针,30个元素 |
huge_list | zend_mm_huge_list* | 巨大块内存链表指针 |
main_chunk | zend_mm_chunk* | 主 chunk |
cached_chunks | zend_mm_chunk* | chunk 链表指针 |
chunks_count | int | chunk 数量 |
peak_chunks_count | int | chunk 最大数量 |
cached_chunks_count | int | 缓存 chunk 数量 |
avg_chunks_count | double | 平均 chunk 数量 |
last_chunks_delete_boundary | int | chunk 清理阈值 |
last_chunks_delete_count | int | 清理时删除的 chunk 数量 |
real_size | size_t | 当前使用的内存大小 |
limit | size_t | 最大可内存大小 |
overflow | int | 内存是否已经溢出 |
以上是 zend_mm_heap
的关键元素,其中前4个用于管理分配与回收,其他用于内存清理与性能监控。
zend_mm_chunk
结构体是 PHP 的基本内存分配单元,每个 chunk 固定2MB,内部按页(page)划分,页的使用状态由位图和映射表记录。内存管理器向操作系统申请内存时,总是以 chunk 为单位;清理释放内存时,也是以 chunk 为单位。主体结构如下:
元素名 | 类型 | 说明 |
heap | zend_mm_heap* | 指向所属 heap |
next | zend_mm_chunk* | 指向前一个 chunk |
prev | zend_mm_chunk* | 指向后一个 chunk |
free_pages | uint32_t | 空闲 page 数量 |
free_tail | uint32_t | 末尾空闲 page 数量 |
num | uint32_t | chunk 的创建序号 |
reserve | char[] | 未使用字段 |
heap_slot | zend_mm_heap | 主 chunk 会用到它 |
free_map | zend_mm_page_map | 64B(512 bit),每个 bit 标记一个 page 的使用状态 |
map | zend_mm_page_info[] | 512个 page,每个 page 分配一个32位整数(4B),共2KB |
zend_mm_page
结构体是内存分配的中间单位,其大小由前文中提到的 ZEND_MM_PAGE_SIZE
常量决定,常值为4KB。page 的结构非常简单:
元素名 | 类型 | 说明 |
bytes | char[ZEND_MM_PAGE_SIZE] | 占4KB内存(不仅当作 char 型使用) |
结构关系图(heap → chunk → page → run)
scss
zend_mm_heap (heap)
│
├── zend_mm_chunk (2MB)
│ │
│ ├── chunk header(结构体头部字段:heap / next / prev / ...)
│ │
│ ├── 第0页:管理页(地图)
│ │ ├── free_map(64B 位图,标记 512 个 page 是否空闲)
│ │ └── map(page 信息表:512 × 4B ≈ 2KB)
│ │
│ └── 第1页 ~ 第511页:可分配页区
│ ├── run(small bins,小块分配区)
│ └── lrun(large block,大块:整页对齐)
│
└── huge block (>2MB)
└── 通过 mmap 独立映射(不占用上述 chunk 页区)
四、启动内存管理
在进行内存分配前,要先启动内存管理器。zend.c
中的 zend_startup()
函数用于启动 PHP 的相关组件,无论是命令行模式、FPM 模式还是调试模式,PHP 在所有运行模式下都会调用该函数。zend_startup()
函数调用 zend_alloc.c
中的 start_memory_manager()
函数来启动内存管理器。
初始化过程主要包括以下函数调用路径:
scss
zend_startup()
└── start_memory_manager()
└── alloc_globals_ctor()
└── zend_mm_init()
└── zend_mm_chunk_alloc_int()
└── zend_mm_mmap()
该路径展示了从全局初始化到堆构建,再到底层内存映射的完整流程。
zend_mm_init()
函数调用 zend_mm_chunk_alloc_int()
函数,分配内存并创建主 chunk,同时在主 chunk 内初始化 zend_mm_heap
实例。由此可见:
- 主 chunk 的大小是
ZEND_MM_CHUNK_SIZE
,值为2MB。因此 PHP 程序只要启动,就会在这里分配一个2MB的空间。 zend_mm_heap
实例不是单独创建的,而是主 chunk 中的一部分。
至此,PHP 的内存管理体系完成初始化,主 chunk 与堆实例一同构成了整个内存分配的起点。
小结
本文从文件结构、核心常量、功能接口、数据结构到初始化流程,解析了 zend_alloc
的设计思路与运行机制。可以看到,PHP 的内存管理体系以 chunk 为核心、page 为基础单元,通过层次化的结构设计和高效的位图标记,实现了对不同大小内存块的精确控制。下一篇文章将深入分析内存分配与回收的细节,包括小块(small block)与大块(large block)的具体实现策略。
如果你对 PHP 内存管理有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~