readfile()在php 7.4中会因输出缓冲和压缩导致内存溢出,正确做法是清空缓冲、fopen()+fread()分块读取+flush()手动流式输出。

直接用 readfile() 或 file_get_contents() 下载大文件,PHP 进程大概率会崩在 Allowed memory size of XXX bytes exhausted —— 不是文件太大,而是输出缓冲或内存加载方式把“流式”变成了“全量塞内存”。
为什么 readfile() 在 PHP 7.4 里也会吃光内存?
很多人误以为 readfile() 是纯流式、不占内存。它确实是边读边输出,但输出要经过 PHP 的输出缓冲层。PHP 7.4 默认启用了 output_buffering(通常为 4096 字节或开启),而像 WordPress、Laravel 等框架还会额外调用 ob_start() 套好几层;再加上 zlib.output_compression = On,整个文件内容就会被缓存在内存里等压缩完再发出去。
结果:一个 100MB 文件,readfile() 尝试往缓冲区塞 100MB → 超过 memory_limit → 直接 fatal error。
关键点:readfile() 本身没错,错的是它运行的环境。
怎么安全地用 fread() 分块传输(推荐零配置方案)
绕过所有缓冲干扰,手动控制读取和输出节奏。这是 PHP 7.4 下最稳定、无需改服务器配置的落地做法。
- 必须先清空所有输出缓冲:
while (ob_get_level()) { ob_end_clean(); } - 设置正确 header:
Content-Type、Content-Disposition、Content-Length缺一不可,否则浏览器可能无法识别下载行为或断连 - 用
fopen($file_path, 'rb')以二进制只读打开,避免换行符转换干扰 - 每次
fread($fp, 1048576)(1MB)后立刻flush(),强制把数据推给客户端 - 别漏掉
fclose($fp)和exit,防止后续代码意外输出乱码
示例片段:
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file_path) . '"');
header('Content-Length: ' . filesize($file_path));
if (ob_get_level()) {
while (ob_get_level()) ob_end_clean();
}
$fp = fopen($file_path, 'rb');
if ($fp) {
while (!feof($fp)) {
echo fread($fp, 1048576);
flush();
}
fclose($fp);
}
exit;
临时调高 memory_limit 的坑与边界条件
PHP 7.4 中 ini_set('memory_limit', '512M') 确实能救急,但它只对「后续内存分配」生效——如果错误发生在 ini_set() 之前(比如自动加载器 require 了巨量文件),就完全无效。
更危险的是这些情况:
-
ini_set('memory_limit', '-1')绝对不能上生产:PHP worker 占住内存不释放,多个并发请求就能拖垮整台机器 - CLI 脚本用
php -d memory_limit=1G script.php更可靠,Web 请求则优先走.user.ini(需user_ini.filename启用) - 调高前务必确认是不是真溢出:用
memory_get_peak_usage(true)在fread循环前后打点,看是否线性增长;如果是,说明还有未释放资源(比如 PDOStatement 没 close)
哪些操作在 PHP 7.4 下特别容易悄悄爆内存?
有些写法在小文件下完全没问题,一到 50MB+ 就原形毕露:
-
file_get_contents($remote_url)下载远程大文件:整个响应体变成字符串塞进内存,哪怕你马上file_put_contents()也晚了 -
$pdo->query($sql)->fetchAll()查几十万行:每行都是数组 + 引用结构,比原始数据体积大 3–5 倍 - 用
json_encode($huge_array)输出 API:编码过程会复制中间结构,峰值内存可能是原数组的两倍 - 循环中反复
new StdClass()且对象间有引用闭环:PHP 7.4 的 GC 对复杂循环引用回收不及时,gc_collect_cycles()可缓解但非根治
真正难排查的不是单次大内存分配,而是变量生命周期失控——比如闭包捕获了超大数组,或事件监听器没解绑,导致对象长期驻留。这类问题不会立刻报错,但会随请求累积,直到某次刚好跨过阈值。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











