PHP 8.2 CLI 脚本内存不降是因Zend MM保留空闲页供复用,非泄漏;真实释放需进程退出或显式关闭资源、分段GC、避免static缓存,并监控RSS而非memory_get_usage()。

PHP 8.2 CLI 脚本运行中内存不降?别指望脚本结束才回收
PHP 8.2 的 CLI 模式(比如宝塔计划任务调用的 php /path/to/script.php)默认不会在脚本中途主动归还内存给操作系统,哪怕你用了 unset() 或 gc_collect_cycles()。这是因为 Zend 内存管理器(Zend MM)会把释放的小块内存保留在进程 heap 中,供后续分配复用——这能提速,但会让 memory_get_usage(true) 看起来“居高不下”。真实释放到系统,往往要等整个进程退出。
哪些操作真能降低实际内存占用?
不是所有“释放”都等于减少 RSS。以下操作在 PHP 8.2 CLI 中有明确效果:
-
unset()+gc_collect_cycles()组合:对循环引用对象、大数组有效,能触发 Zend MM 回收内部页(page),但不一定立即交还给 OS - 显式关闭资源句柄:如
fclose($fp)、$pdo = null、curl_close($ch),可立刻释放对应内核资源和关联缓冲区 - 分段处理 +
gc_disable()/gc_enable()切换:长循环中每处理 N 条数据后禁用再启用 GC,可避免 GC 频繁扫描拖慢速度,同时让周期性回收更可控 - 避免全局变量缓存:CLI 脚本生命周期内,
static变量或$GLOBALS引用的大对象会全程驻留,应改用函数局部作用域
宝塔计划任务跑 PHP 8.2 脚本,怎么写才不容易 OOM?
宝塔后台执行的是短时 CLI 进程,重点不在“持续释放”,而在“单次不爆”。常见坑和对策:
- 别在脚本里用
ini_set('memory_limit', '2G')—— 宝塔计划任务默认走系统 php.ini,CLI 的memory_limit通常已设为 -1(无限制),强行设大会掩盖真实泄漏 - 数据库查询必须分页或流式:用
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false+fetch()循环,而非fetchAll();否则百万行直接吃光内存 - 日志/临时文件写完立刻
unset($big_log_array)并gc_collect_cycles(),尤其在循环体末尾 - 如果脚本需长时间运行(>5 分钟),考虑拆成多个子进程,用
exec("nohup php child.php > /dev/null 2>&1 & echo $!")启动,让父进程退出释放全部资源
为什么 memory_get_usage(true) 和系统 top 看到的 RSS 差很多?
这是最常被忽略的一点。PHP 8.2 的 memory_get_usage(true) 返回的是 Zend MM 当前从系统申请的总 heap 大小(含空闲页),而 top 的 RSS 是进程实际占用的物理内存页。两者差异来自:
立即学习“PHP免费学习笔记(深入)”;
- Zend MM 不会把小块空闲页立即
madvise(MADV_DONTNEED)还给系统,除非 heap 整体收缩(比如大量大对象释放后触发zend_mm_heap->size下降) - OPcache、PCRE JIT 缓存、扩展自定义分配(如 Redis 扩展的连接池)也计入 RSS,但不计入
memory_get_usage() - Linux 的
vm.swappiness设置会影响内核何时回收进程匿名页,和 PHP 本身无关
所以监控 CLI 内存是否健康,应该看 ps aux --sort=-%mem | head -5 中该 PHP 进程的 RSS 是否随任务批次稳定回落,而不是盯着 memory_get_usage() 数值。



















