内存管理篇(一)· zend_alloc 的基本概念

前言

zend_alloc 是 PHP 最关键的内存管理工具,本文将首先介绍zend_alloc的整体架构与原理,为深入理解 zend_alloc 的实现奠定基础,然后在后续章节中逐步展开相关文件、主要功能、数据结构以及初始化流程。

注:本文所指"用户态内存",是指 PHP 进程在用户空间中由 Zend 引擎自行管理的堆内存,用于脚本执行时的变量、结构体、字符串等数据,不包含内核态或共享内存区域。

新的内存管理器(从 PHP 5.2 开始使用)会从整体上减少内存分配,并提升内存分配速度。

zend_alloc 是为 PHP 设计的、对现代 CPU 缓存友好的内存管理工具。大部分构思来源于 jemalloctcmalloc 的实现。

所有的内存分配被划分成三种:这些分类依据的是分配块的大小,每种类型对应不同的分配策略。

  • 巨大块(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 实例。由此可见:

  1. 主 chunk 的大小是 ZEND_MM_CHUNK_SIZE,值为2MB。因此 PHP 程序只要启动,就会在这里分配一个2MB的空间。
  2. zend_mm_heap 实例不是单独创建的,而是主 chunk 中的一部分。

至此,PHP 的内存管理体系完成初始化,主 chunk 与堆实例一同构成了整个内存分配的起点。

小结

本文从文件结构、核心常量、功能接口、数据结构到初始化流程,解析了 zend_alloc 的设计思路与运行机制。可以看到,PHP 的内存管理体系以 chunk 为核心、page 为基础单元,通过层次化的结构设计和高效的位图标记,实现了对不同大小内存块的精确控制。下一篇文章将深入分析内存分配与回收的细节,包括小块(small block)与大块(large block)的具体实现策略。

如果你对 PHP 内存管理有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~

相关推荐
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082855 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe5 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5