PHP 8.3 多进程下内存不降是常态而非泄漏,主因是 Zend 内存管理器缓存小块内存;需显式释放资源句柄、避免共享 static 变量,并在子进程退出前调用 gc_collect_cycles() 和 gc_mem_caches() 清理碎片。

PHP 8.3多进程下内存不降是常态,不是泄漏
PHP 8.3 的 CLI 多进程(如 pcntl_fork())中,子进程退出后 memory_get_usage() 显示的内存不回落,基本不是泄漏,而是 Zend 内存管理器的缓存行为——小块内存(pcntl 子进程没有这套清理机制。
常见误判场景:unset($bigArray); gc_collect_cycles(); 后内存没变,就以为没释放。其实变量已销毁、引用计数归零,只是底层内存池没交还系统。
- 不要依赖
memory_get_usage()判断子进程是否“真泄漏”,它反映的是当前进程堆内分配量,不是 RSS 实际占用 - 真正该看的是系统级指标:用
ps -o pid,rss,vsz -p $pid或smem -p $pid观察子进程退出后 RSS 是否回落 - 若 RSS 持续上涨(比如每轮 fork + work 后 RSS 增加 5MB 且不降),才需怀疑真实泄漏
子进程退出前必须手动清理的三类资源
PHP 自动回收变量,但以下资源不进 GC 流程,子进程 exit 前不处理,就会累积到父进程或耗尽系统 fd/句柄:
-
PDOStatement对象:即使$stmt被unset,未调$stmt->closeCursor()或$stmt->free(),结果集缓冲区仍驻留 - cURL 句柄:
curl_close($ch)必须显式调用,否则底层 socket 和 buffer 不释放 - 文件指针:
fclose($fp)不能省;用fopen打开又不关,在高并发 fork 下极易触发 “Too many open files”
推荐模式:在子进程逻辑末尾统一 try...finally 块兜底:
立即学习“PHP免费学习笔记(深入)”;
try {
$pdo = new PDO(...);
$stmt = $pdo->query("SELECT ...");
// ... 处理
} finally {
$stmt?->closeCursor();
$pdo = null;
gc_collect_cycles();
}
避免父子进程共享资源引发的隐性泄漏
多进程最易踩的坑不是内存,而是“以为隔离了,其实没断干净”。PHP 8.3 中这些操作会导致子进程意外持有父进程资源:
- fork 前已打开的
file_get_contents('php://input')或STDIN流,子进程继承 fd 但可能读取错位 - 使用
static变量缓存 DB 连接或大数组,fork 后子进程拷贝的是指针副本,修改不会影响父进程,但内存占用双份 - 通过
register_shutdown_function()注册的回调,在子进程 exit 时仍会执行——若回调里有log::write($hugeData),就白忙活
安全做法:pcntl_fork() 后立即在子进程中重置关键状态:
if ($pid === 0) {
// 子进程
gc_disable(); // 避免父进程 GC 状态干扰
error_reporting(0); // 关闭错误输出,防止写入父进程 stdout
fclose(STDIN); fclose(STDOUT); fclose(STDERR); // 关闭继承的流
// 重新初始化需要的资源,不复用父进程的 static 缓存
exit(0);
}
真正要调 gc_mem_caches() 的时候
当确认 RSS 持续上涨、排除了资源句柄和循环引用后,大概率是内存碎片。PHP 8.3 已内置 gc_mem_caches(),它是唯一能强制清空 Zend 小内存缓存池的函数,专治 fork 后“unset 了但 RSS 不降”的问题。
- 只在子进程即将退出前调用,不要在循环中高频使用(有性能开销)
- 必须搭配
gc_collect_cycles()使用,顺序不能反:gc_collect_cycles(); gc_mem_caches(); - 注意:此函数仅对 CLI 模式有效,Web SAPI 下无意义
一个稳妥的子进程收尾模板:
if ($pid === 0) {
// ... 工作逻辑
unset($bigData, $tempFiles);
gc_collect_cycles();
gc_mem_caches();
exit(0);
}
别指望靠 unset 或 GC 让 RSS 归零——那是操作系统的事。重点是确保每次 fork 出来的子进程,退出时没留下任何可追踪的资源残留。碎片问题用 gc_mem_caches() 拦腰一砍,比换 jemalloc 实在得多。



















