CLI脚本内存持续上涨主因是未释放资源、循环引用或自动加载失控,需用memory_get_peak_usage(true)多轮压测(100–500轮),监控delta是否稳定增长;重点检查get_included_files()数量、闭包捕获、双向对象引用及JIT/OPcache泄漏,gc_collect_cycles()配合unset()可延缓泄漏。

CLI 脚本内存持续上涨,基本不是 memory_limit 设低了,而是脚本自身在“悄悄吃内存”——不释放、不清理、不打断引用,PHP 的 GC 在长生命周期里根本来不及或无法回收。
用 memory_get_peak_usage(true) 做循环压测打点
单次执行看不出问题,必须跑多轮。尤其对队列消费者、定时任务这类常驻逻辑,100–500 轮是底线:
- 在循环体外记录基准:
$start = memory_get_peak_usage(true) - 每轮末尾计算差值:
$delta = memory_get_peak_usage(true) - $start,直接输出或写入日志 - 若
$delta稳定 +8KB/+16KB/每次,就是真实泄漏;若波动在 ±2KB 内,属正常 Zend 内存池抖动 - 别用
memory_get_usage()默认调用——它返回的是当前分配器视图,含大量未归还的缓存块,误导性极强
检查 get_included_files() 和自动加载失控
文件越载越多,内存就涨得越稳。很多框架在 CLI 下因配置缺失或调试开关残留,导致每次循环都重新解析全部类文件:
- 执行
count(get_included_files()),若长期 >150,说明自动加载已失控 - 检查是否在循环内调用了
require/include动态路径,或Composer\Autoload\ClassLoader::loadClass()被反复触发 - 确认
opcache.enable_cli=1已启用,否则 CLI 模式下 OPcache 默认关闭,每次都是冷解析 - 临时加一句
var_dump(array_slice(get_included_files(), -5)),看最后几个是不是每次都在变
揪出循环引用和闭包捕获的大对象
GC 对简单环能处理,但对嵌套深、含资源句柄、或被全局变量间接持有的环,大概率失效:
立即学习“PHP免费学习笔记(深入)”;
- 用
debug_zval_dump($obj)查看关键对象的refcount和is_ref;is_ref=1且refcount > 1是高危信号 - 重点查
use ($hugeData)的闭包——它会把整个变量副本进闭包作用域,且生命周期绑定到闭包本身 - 对象双向绑定(如
$model->relation和$relation->model)必须手动断开:$model->relation = null,不能只靠unset() - PHP 7.4+ 优先用
WeakReference::create($obj)替代强引用,尤其用于事件监听、缓存代理等场景
别忽略 JIT 和 OPcache 的协同泄漏(PHP 8.5+)
CLI 常驻进程 + JIT 启用 = 内存缓慢爬升的温床。错误日志可能完全静默,只看到 RSS 持续上涨:
- 运行
opcache_get_status(),重点看jit['function_count']和jit['memory_usage']:若前者稳定增长、后者远超opcache.jit_buffer_size,说明 JIT 编译残留严重 - 临时禁用 JIT:
opcache.jit=0,观察 RSS 是否回归平稳;若恢复,问题基本锁定 JIT 层 - 检查是否误启了
opcache.jit_debug=0x20,它会在 error_log 输出 JIT 分配细节,可定位哪类函数高频触发编译 - 确认 Runtime 缓存目录(如 ThinkPHP 的
Runtime/)没被循环清空——每次清空都会让 OPcache 失效并触发重复解析
最易被跳过的点:CLI 脚本里 gc_collect_cycles() 不是万能药,但它配合 unset() 在每轮末尾调用一次,确实能延缓泄漏速度;而 __destruct() 和 gc_disable() 基本无效,甚至有害。



















