一、zend_alloc中的内存的自动回收机制
在 PHP 内存管理体系中,内存的自动回收是一项用来防止缓存块无限增长的自我调节机制。
在正常运行的 PHP 环境中,大多数场景都会开启内存使用限制。当分配器发现当前已用内存量(heap->real_size)即将触顶,而缓存中又没有可复用的 chunk 时,系统会主动触发一次清理,以释放掉部分空闲内存。
这一机制主要由 zend_mm_gc() 函数实现。它会在多个关键路径中被调用,包括:
- zend_mm_alloc_pages()
- zend_mm_realloc_huge()
- zend_mm_alloc_huge()
其核心目标是:在不中断程序执行的前提下,清理过多占用的空闲内存。
二、整理小块内存
整理空闲链表,归还未使用的小块内存串。
自动回收的第一步是清理空闲列表(heap->free_slot[])。
在 PHP 的内存分配体系中,小块内存的复用非常频繁,因此其释放策略格外精细。系统会遍历每个空闲链表,统计每个小块串中空闲块的数量,并判断是否存在整串空闲的 page。如果发现整串未被使用,会更新标记,为后续回收做准备。
zend_mm_free_slot *p, **q;
p = heap->free_slot[i];
// 遍历一串所有的小块内存
while (p != NULL) {
// 取回所在 chunk 的指针
chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(p, ZEND_MM_CHUNK_SIZE);
page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE); // 找到所属 page 序号
info = chunk->map[page_num]; // 取得 page 地图信息
// 如果存在 LRUN 标记,说明这是一个横跨多个 page 的串
if (info & ZEND_MM_IS_LRUN) {
page_num -= ZEND_MM_NRUN_OFFSET(info); // 找到串的第一个 page
info = chunk->map[page_num]; // 更新地图信息
}
free_counter = ZEND_MM_SRUN_FREE_COUNTER(info) + 1; // 空闲计数 +1
// 如果整串 page 全部空闲
if (free_counter == bin_elements[i]) {
has_free_pages = true; // 标记为可回收
}
chunk->map[page_num] = ZEND_MM_SRUN_EX(i, free_counter); // 记录空闲数量
p = p->next_free_slot; // 遍历下一个 small 块
}
这一步并不直接释放内存,而是更新统计信息,为下一轮整理做准备。
三、把闲置的小块内存串从链表中摘除
在第二轮遍历中,系统会再次扫描链表,将整串空闲的小块内存从链中摘除。
这里使用了一个非常巧妙的技术——**指向指针的指针(zend_mm_free_slot q) ,使得删除操作既高效又简洁。
q = &heap->free_slot[i]; // q 始终指向可能被更新的指针
p = *q; // 链表的第一个元素
while (p != NULL) {
if (ZEND_MM_SRUN_FREE_COUNTER(info) == bin_elements[i]) {
// 如果整串都空闲,将其从链表中移除
p = p->next_free_slot;
*q = p; // 更新上层指针
} else {
// 若未完全空闲,则继续向后遍历
q = &p->next_free_slot;
p = *q;
}
}
通过双指针操作,Zend 在 O(1) 时间内完成链表重排,显著提升了回收效率。
最终,未使用的小块内存串全都从闲置内存链表中摘除了。
四、整理检查并回收空闲的 chunk
在回收完小块内存后,系统会扫描所有 chunk,将空闲的整块内存归还给系统。
这一步从 main_chunk 开始,依次遍历每个 chunk,对每个 chunk 执行两项检查:
- 遍历 page,找出整串空闲的小块内存串并释放;
- 检查整个 chunk 是否完全空闲,若是,则调用 zend_mm_delete_chunk() 回收该 chunk。
chunk = heap->main_chunk; // 从 main_chunk 开始
do {
i = ZEND_MM_FIRST_PAGE; // 从第一页起
while (i < chunk->free_tail) { // 遍历所有已使用的 page
if (zend_mm_bitset_is_set(chunk->free_map, i)) { // 当前 page 已使用
info = chunk->map[i];
if (info & ZEND_MM_IS_SRUN) { // 小块内存
int bin_num = ZEND_MM_SRUN_BIN_NUM(info);
int pages_count = bin_pages[bin_num]; // 连续使用的page数量
// 如果整串page全部空闲(空闲的数量 = 配置数量)
if (ZEND_MM_SRUN_FREE_COUNTER(info) == bin_elements[bin_num]) {
zend_mm_free_pages_ex(heap, chunk, i, pages_count, 0); // 回收这串 page
collected += pages_count; // 记录回收数量
} else {
// 有使用块,清空前次统计
chunk->map[i] = ZEND_MM_SRUN(bin_num);
}
i += bin_pages[bin_num]; // 跳过已处理的串
} else {
// 非小块(大块)内存
i += ZEND_MM_LRUN_PAGES(info);
}
} else {
i++;
}
}
// 检查当前 chunk 是否完全空闲
if (chunk->free_pages == ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE) {
zend_mm_chunk *next_chunk = chunk->next;
zend_mm_delete_chunk(heap, chunk); // 回收空 chunk
chunk = next_chunk;
} else {
chunk = chunk->next; // 处理下一个
}
} while (chunk != heap->main_chunk);
设计思路解析
-
跳跃式遍历:
并不会逐页扫描,而是以“page 串”为单位跳跃式前进。只要确认一个串的首 page,就能判断整段是否回收。
-
轻量回收:
这套回收机制只清理空闲链表和空 chunk,不做碎片整理,因而执行代价极低,回收能力也比较有限。
-
按需触发:
zend_mm_gc() 在分配阶段被动触发,属于“机会式清理”机制,确保性能优先。
整个机制是一种“温和型回收”:速度快,不阻塞执行;但无法处理深层碎片;如果要做进一步的回收,会让分配内存时速度变慢——时间和空间的平衡,总是无法两全齐美。
五、小结
内存回收机制的逻辑相较于分配过程更为直接,但实现上仍有不少精妙的优化。
zend_mm_gc() 的存在正是为了在性能与空间利用率之间取得平衡。
它不追求“彻底清理”,而追求“低成本回收”。这是 PHP 内存管理中最具工程智慧的部分之一。
总结要点:
- 自动回收由 zend_mm_gc() 触发,在分配内存时自动执行。
- 它先整理空闲的小块内存链,再检查并回收整块 chunk。
- 整体机制轻量、快速、非阻塞,符合 PHP 的运行特性。
- 本质是“懒式清理”策略:只在必要时释放,以保持高性能。
如果你对 PHP 内存管理有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~
本文项目地址:github.com/xuewolf/php…