导出一个几万行的统计表,页面转了十几秒之后白屏,错误日志里留下这么一行:
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月。