齿轮战斗免广告
163.46 MB · 2025-11-05
释放指针所指向、已不再使用的内存,是内存管理闭环中的关键一步。Zend 的释放路径保持了与分配对称的结构:从统一入口入手,根据块规模分派到对应的子流程,确保正确地回收 page 与元信息。
efree() -> _efree() -> zend_mm_free_heap()
| 内存大小 | 调用方法 |
|---|---|
| 小块内存 | zend_mm_free_small() |
| 大块内存 | zend_mm_free_large() |
| 巨大块内存 | zend_mm_free_huge() |
释放前必须先识别待释放指针所对应的块类型(small/large/huge),否则无法进入正确的回收分支。Zend 将这一判定收敛在统一入口 zend_mm_free_heap() 中:
// 统一释放入口:只接收指针,不显式传入大小
static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr)
可以看出,该函数只接收“指针”而非“尺寸”。尺寸与类型由函数内部按规则推断:
下面是 zend_mm_free_heap() 的业务逻辑(带注释):
// 计算该指针相对 chunk 起点的对齐偏移(按 ZEND_MM_CHUNK_SIZE 对齐)
size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE);
if (UNEXPECTED(page_offset == 0)) {
// 偏移为 0:说明指针与 chunk 起点对齐 → 视作 huge(整 chunk 分配)
if (ptr != NULL) {
zend_mm_free_huge(heap, ptr); // 释放巨大块(huge)
}
} else {
// small / large:都不会占用 chunk 的第一个 page,因此一定“非零偏移”
// 取回该指针所在的 chunk 基址
zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE);
// 将字节级偏移换算为 page 编号
int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE);
// 读取页级 map 的语义位与携带信息(bin 号 / 连续页数 / 偏移等)
zend_mm_page_info info = chunk->map[page_num];
if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
// SRUN:小块内存的首个 page
// 从 map 中取出 bin 号,走 small 回收分支(需要 bin 编号以维护空闲链等)
zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
} else /* if (info & ZEND_MM_IS_LRUN) */ {
// 非 SRUN:按 large 处理,页数来自 LRUN 段
int pages_count = ZEND_MM_LRUN_PAGES(info);
zend_mm_free_large(heap, chunk, page_num, pages_count);
}
}
巨大块(huge)以整块系统映射为单位进行分配与释放。其释放路径精炼而对称:先从“巨大块链表”中摘除对应节点,拿到实际尺寸;再将这段映射交回系统。
核心逻辑在 zend_mm_free_huge() 中,伪代码如下(保留关键注释):
// 从巨大块链表中删除对应节点,返回该块的实际大小(Bytes)
size_t size = zend_mm_del_huge_block(heap, ptr);
// 真正的释放发生在这里:把这段映射交还给系统
zend_mm_chunk_free(heap, ptr, size);
zend_mm_del_huge_block() 负责遍历并定位 zend_mm_huge_list 中指向 ptr 的节点(关于该链表与节点结构,可参见“巨大块内存分配”章节)。定位后完成摘链,并把节点里记录的 size 返回。完成这一步之后,zend_mm_free_huge() 才具备“知道要还多少”的充足信息。
随后进入 zend_mm_chunk_free():该函数会调用底层的 zend_mm_munmap(),将这段以系统页为单位建立的映射解除,归还给操作系统。由于 huge 块本就直接来自系统映射,其释放没有 chunk/page 粒度上的页表维护负担,路径最短,副作用最小。
大块(large)释放以“页串(pages)”为粒度,依据 map/bitset 恢复占用标记,再按策略决定是否将空 chunk 缓存或直接归还系统。核心入口是:
// 除 heap 外还需要:所属 chunk、起始页号、页数
static zend_always_inline void zend_mm_free_large(
zend_mm_heap *heap,
zend_mm_chunk *chunk,
int page_num,
int pages_count
);
调用路径:
zend_mm_free_large()
→ zend_mm_free_pages()
→ zend_mm_free_pages_ex()
→ zend_mm_delete_chunk()
→ zend_mm_chunk_free()
→ zend_mm_munmap()
zend_mm_free_pages_ex() 负责完成页级回收与必要的边界推进。相比 zend_mm_free_large(),该函数多了一个“是否删除空 chunk”的开关:
// free_chunk:是否删除空 chunk(1=允许删除;0=仅回收页)
static zend_always_inline void zend_mm_free_pages_ex(
zend_mm_heap *heap,
zend_mm_chunk *chunk,
uint32_t page_num,
uint32_t pages_count,
int free_chunk
)
核心业务(保留关键注释):
chunk->free_pages += pages_count; // 增加可用 page 数
// 更新 bitset 地图,把相应的 page 标记为空闲
zend_mm_bitset_reset_range(chunk->free_map, page_num, pages_count);
// 重置 map 的起始项(后续若是 SRUN/NRUN/LRUN,会在分配时重新写入)
chunk->map[page_num] = 0;
// 如果被删除的页串末尾 == 已用页的末尾(后面全是空闲)
if (chunk->free_tail == page_num + pages_count) {
// 推进尾指针,使“可跳过区”增大
chunk->free_tail = page_num;
}
// 若允许删除 chunk,且该 chunk 非 main_chunk 且已完全空闲
if (free_chunk
&& chunk != heap->main_chunk
&& chunk->free_pages == ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE) {
zend_mm_delete_chunk(heap, chunk); // 尝试删除/缓存该 chunk
}
zend_mm_delete_chunk() 并不总是立刻释放内存,而是优先将空 chunk 放入 缓存链表,以便后续复用,减少系统调用与序号回退带来的管理成本。
若将要删除的 chunk 的序号大于缓存链表头的序号,则先释放原缓存头,再把当前 chunk 放到缓存头。此举可确保“序号最大的 chunk 要么在用,要么在缓存”,避免新建时序号回退或重复。
业务逻辑(保留注释,略去非关键边角):
// 先把自己从 chunk 环上摘掉
chunk->next->prev = chunk->prev;
chunk->prev->next = chunk->next;
heap->chunks_count--; // 正在使用的 chunk 数 -1
// 满足“加入缓存”的任一条件
if (heap->chunks_count + heap->cached_chunks_count < heap->avg_chunks_count + 0.1
|| (heap->chunks_count == heap->last_chunks_delete_boundary
&& heap->last_chunks_delete_count >= 4)) {
heap->cached_chunks_count++; // 缓存计数 +1
chunk->next = heap->cached_chunks; // 头插到缓存链表
heap->cached_chunks = chunk;
} else {
// 不满足缓存条件,考虑直接释放
heap->real_size -= ZEND_MM_CHUNK_SIZE;
if (!heap->cached_chunks) {
// 维护“上次清理阈值/计数”
if (heap->chunks_count != heap->last_chunks_delete_boundary) {
heap->last_chunks_delete_boundary = heap->chunks_count;
heap->last_chunks_delete_count = 0;
} else {
heap->last_chunks_delete_count++;
}
}
// 若当前 chunk 序号更大,则直接释放当前;否则释放缓存头,把当前放到缓存头
if (!heap->cached_chunks || chunk->num > heap->cached_chunks->num) {
zend_mm_chunk_free(heap, chunk, ZEND_MM_CHUNK_SIZE); // 直接释放
} else {
chunk->next = heap->cached_chunks->next; // 接到缓存第二个
zend_mm_chunk_free(heap, heap->cached_chunks, ZEND_MM_CHUNK_SIZE); // 释放原缓存头
heap->cached_chunks = chunk; // 当前成为缓存头
}
}
当走到 zend_mm_chunk_free() 时,说明已决定不缓存该 chunk。此时会调用 zend_mm_munmap() 将整段映射解除,内存真正回到操作系统控制之下。
小块(small)块的释放不涉及系统归还,核心动作是把这块闲置内存挂回对应 bin 的空闲链表头部,以便后续 O(1) 速度复用。入口函数为 zend_mm_free_small(),业务极简:
zend_mm_free_slot *p;
p = (zend_mm_free_slot*)ptr; // 指针转成 zend_mm_free_slot(单链节点)
p->next_free_slot = heap->free_slot[bin_num]; // 新节点指向当前空闲链表的头
heap->free_slot[bin_num] = p; // 更新链表头指针到本节点
不难看出,这里只是回收到空闲链表,并未真正释放内存页。
批量预分配,减少频繁开销
zend_mm_alloc_small_slow() 会按 ZEND_MM_BINS_INFO() 的配置“一次分一串”,将若干小块打包准备,显著降低“分配调用次数”。
空闲链表管理,已用块分散在各自上下文*
空闲的小块被串成单链挂在 heap->free_slot[bin],已使用的小块则由其所属对象/数组/变量持有指针,二者彼此独立、互不干扰。
高频路径保持常数复杂度
从空闲链表弹出(分配)与向链表头插(回收)都是常数时间,满足热点路径对吞吐的极致要求。
在 Zend 的内存管理体系中,释放过程与分配过程是严格对称的。每一次释放,既是资源的归还,也是未来复用链条上的一次“再布置”。
这种分层式的释放策略,使 Zend 内存管理器兼顾了三点:
如果你对 PHP 内存管理有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~
本文项目地址:github.com/xuewolf/php…