PHP报错Allowed memory size of 134217728 bytes exhausted的解决方法(memory_limit调整、memory_get_usage定位与内存泄漏排查)

导出一个几万行的统计表,页面转了十几秒之后白屏,错误日志里留下这么一行:


PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes) in /var/www/app/export.php on line 87


134217728字节正好是128M,也就是memory_limit的默认值。括号里那个12288字节最容易被读错,它只是最后一次分配失败时要申请的量,跟此前已经吃掉多少内存无关。见过有人照着12288去查,越查越偏。

报错行号同样不是凶手。87行只是压垮上限的那一次分配,真正把内存吃干的地方往往在它上面的某个循环里。

先确认改的是不是正在生效的那份配置

调大memory_limit这件事,一半的失败发生在改错了文件。PHP命令行与PHP-FPM加载的不是同一个php.ini,改了命令行那份,网页这边一点变化都不会有。让PHP自己报路径最省事:


php --ini
php -i | grep memory_limit
php -r 'echo ini_get("memory_limit"), PHP_EOL;'


Debian与Ubuntu的包结构比较固定,命令行那份在/etc/php/8.3/cli/php.ini,Web那份在/etc/php/8.3/fpm/php.ini,目录里带的就是大版本号。Rocky、AlmaLinux这类通常是/etc/php.ini主配置加/etc/php-fpm.d/www.conf的pool配置。要确认Web侧真正读的是哪一份,在站点根目录下放个临时脚本输出phpinfo(),看Loaded Configuration File那一行,查完删掉。

能改的五个地方,生效范围不一样

1、php.ini里的memory_limit,影响该SAPI下的所有脚本。改完PHP-FPM要重启服务才生效,命令行下一次执行自动生效:


memory_limit = 256M
sudo systemctl restart php8.3-fpm


2、pool配置里用php_admin_value,只对这个池生效,适合一台机器上跑多个站点、只想给其中一个开口子的情况:


php_admin_value[memory_limit] = 512M


php_admin_value带锁定效果,脚本里再写ini_set就改不动了,这点经常被忽略——遇到"明明ini_set了还是报128M"的情况,先去pool配置里搜有没有这一行。

3、站点根目录放.user.ini,不碰服务器配置时用它:


memory_limit = 256M


这个文件有缓存,默认300秒才重新读一次,改完不会立刻生效,急着验证的时候容易以为没写对。它只在CGI与FPM模式下有效,mod_php不认。

4、脚本开头ini_set,只对单次执行有效:


ini_set('memory_limit', '1024M');


它救不了在它之前就已经吃掉大半个限额的代码,也突破不了php_admin_value设的顶。

5、命令行临时覆盖,跑一次性的导入导出脚本时用这个最干净,不用动任何配置文件:


php -d memory_limit=1G import.php


顺带提一句,Apache下写在.htaccess里的php_value在PHP-FPM环境完全无效,因为请求根本不经过Apache的PHP模块。之前接手过一个迁到FPM之后限制失效的站点,查了半天发现限制一直写在.htaccess里。

先别急着调大,看看是什么在吃内存

128M跑不动一个导出脚本,多数时候不是限额太小,是写法太重。实测下来最常见的四类:

把整个文件读进变量再处理。file_get_contents读一个500M的日志,内存就得跟着涨500M,换成逐行读,占用基本稳在几兆:


$fh = fopen($file, 'rb');
while (($line = fgets($fh)) !== false) {
    // 逐行处理
}
fclose($fh);


数据库一次取全表。fetchAll()把几万条记录全变成数组塞进内存,改成while循环里fetch()逐条处理,或者用生成器yield往外吐,效果立竿见影。

循环里往同一个数组不停追加,还夹着嵌套查询。这种脚本跑一万行和跑十万行的内存曲线是两条完全不同的线,得按批次切分,每批处理完unset掉临时数组。

GD处理大图。imagecreatefromjpeg()解码一张6000×4000的照片,占用按宽×高×通道数算,几十兆起步,而且imagecopyresampled过程中还要再开一块。批量缩略图脚本不主动imagedestroy(),几次循环就能撞上限。

给内存打点,找到具体那一行

猜不如测。在可疑的几段前后各插一行输出,很快就能锁定位置:


printf("A %.1fM\n", memory_get_usage(true) / 1048576);
// 可疑代码
printf("B %.1fM\n", memory_get_usage(true) / 1048576);
printf("peak %.1fM\n", memory_get_peak_usage(true) / 1048576);


脚本崩得太快、来不及加打点的话,用register_shutdown_function兜住,退出时把峰值打出来:


register_shutdown_function(function () {
    $e = error_get_last();
    if ($e) {
        error_log(sprintf("peak=%.1fM %s at %s:%d",
            memory_get_peak_usage(true) / 1048576,
            $e['message'], $e['file'], $e['line']));
    }
});


还有一种取巧办法:临时把限额设成-1放开跑一遍,看峰值到底有多高,再回来定一个合理的数。命令行下php -d memory_limit=-1就行,线上环境别这么干。

这里有个细节,memory_get_usage()传true和不传true差得挺多,前者是向系统申请的量,后者是实际用到的量。判断离限额还有多远,得看传true的那个。

改多大合适

没有通用答案,看这台机器上PHP-FPM能开几个子进程。假设容器给了2G内存,每个worker峰值按80M算,pm.max_children就别超过20,否则内存超限之后是OOM Killer出场,整个进程被杀掉,日志里只剩一条Killed,比PHP的报错难查得多。

Web请求给256M或者512M算比较常见的取值,一次性脚本给1G到2G也正常。真要放开写成-1,只建议用在命令行脚本上,Web侧放开等于把整台机器的内存交给一个请求处置。

PHP-FPM还有个相关参数pm.max_requests,设成500或者1000让子进程处理完这么多请求后重启,能回收掉那些释放不干净的扩展内存。它是兜底手段,不能替代把内存泄漏的写法改掉。

新版本多了一道上限

PHP 8.5.0、8.4.15、8.3.29,这些版本里有一批是2025年11月20日前后发的安全更新。PHP 8.5.0起新增了max_memory_limit这个启动期参数,默认值-1表示不限制,一旦在php.ini里设成比如512M,脚本里的ini_set再怎么调也超不过去。共享主机环境下这个参数很有用,自己独享的服务器上如果发现ini_set失效,除了查php_admin_value,还要看一眼这个值。

截至2026年10月,仍在活跃支持的分支是PHP 8.4、8.5、8.3,其中8.3只剩安全修复,8.2已经在2025年底结束支持。老分支上的脚本撞到内存问题时,升级版本本身有时就能缓解——新版本在数组与字符串处理上的内存开销确实降了一些。


数据截至2026年10月。