根本原因在于脚本生命周期管理不当、资源未释放、循环引用及不合理数据加载;需通过流式查询、显式释放引用、限制静态缓存、设置pm.max_requests重启worker等手段治理,而非仅调大memory_limit。

PHP 高并发下内存不足(Allowed memory size exhausted)不是单纯调大 memory_limit 就能解决的,根源往往在脚本生命周期管理、资源未释放、循环引用或不合理的数据加载方式。
为什么 increase memory_limit 只是掩耳盗铃
临时把 memory_limit 从 128M 改成 512M,可能让报错消失,但并发一上来,OOM killer 仍会杀掉 PHP-FPM 子进程,甚至拖垮整个服务器。更危险的是:掩盖了真实泄漏点——比如一个 foreach 循环里反复 new 对象却不 unset,或 PDO 查询后没调用 $stmt->closeCursor()。
- PHP-FPM 的
pm.max_children是按内存预估的,盲目调高memory_limit会导致实际可并发数暴跌 - CLI 脚本中
memory_limit = -1更危险——内存持续增长直到系统 kill -
memory_get_usage(true)和memory_get_peak_usage(true)必须在关键路径前后打点,否则无法定位峰值来源
大数组和查询结果集是内存黑洞
一次 PDO::query() 返回几万行,用 fetchAll() 全部载入内存,极易爆掉。尤其 JSON 输出前还做 array_map 处理,每行都复制一份数组引用。
- 改用
fetch()或fetchColumn()流式读取,处理完立刻丢弃引用 - 对大数据导出场景,用
yield写生成器函数,避免一次性构建完整数组 - 禁用
PDO::ATTR_EMULATE_PREPARES(设为false),防止 MySQL 驱动在内存中缓存整张结果集 - 查完立刻调
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC),避免默认FETCH_BOTH造成双倍字段存储
对象生命周期和引用计数必须手动干预
PHP 7+ 虽有 Zend GC,但对闭包、静态属性、全局数组里的对象引用仍不敏感。常见陷阱:类里存了 self::$instances[] = $this 却不清理;或用 function ($x) use ($largeArray) { ... } 捕获大变量。
立即学习“PHP免费学习笔记(深入)”;
- 显式调用
unset($obj)后再置为null,确保引用计数归零 - 避免在循环内创建匿名函数,改用普通方法或提前绑定
- 静态缓存(如
static $cache = [])必须加大小限制 + LRU 清理逻辑,不能无上限堆积 - 用
xdebug_debug_zval()查看变量是否被意外引用(需 Xdebug 开启)
PHP-FPM 进程复用加剧内存累积
FPM worker 处理完请求后不会立即销毁全部变量——静态变量、OPcache 中的脚本结构、扩展分配的持久内存都会残留。多个请求串行打到同一 worker,内存只增不减。
- 设置
pm.max_requests = 500~1000(非 0),强制 worker 达限后重启,释放累积内存 - 禁用
opcache.enable_cli=1(CLI 场景),防止 OPcache 在长期运行脚本中越积越多 - 避免在
__construct或init方法中加载全量配置文件或大字典,改用按需加载(lazy load) - 检查扩展是否泄漏:用
php --ri opcache看opcache.memory_consumption是否异常高;用php --ri apcu确认apc.shm_size是否超配
真正卡住人的,往往是某个 foreach 嵌套里忘了 unset($row),或者日志组件把整个 $_SERVER 数组塞进了单例的 debug stack —— 这些细节不打内存快照根本看不见。











