内存溢出
一、什么是内存溢出
内存溢出(Memory Overflow / Out of Memory, OOM)指程序在申请内存时,没有足够的空间供其使用。在 PHP 中,通常表现为:
- 脚本超出
memory_limit设置(默认 128M),抛出Fatal error: Allowed memory size of X bytes exhausted - 系统级 OOM:PHP 进程消耗了操作系统分配的所有内存,被内核强制杀死(常见于常驻进程如 Swoole、Workerman)
二、如何排查内存溢出?
1. 查看错误日志
PHP 错误日志中会直接记录内存耗尽的位置和当前内存使用量。
系统日志(/var/log/messages 或 journalctl -xe)可看到是否因 OOM Killer 杀死了进程。
2. 使用内存分析工具
Xdebug 的追踪功能
xdebug.profiler_enable = 1 xdebug.profiler_output_dir = /tmp生成 cachegrind 文件,用 QCacheGrind 或 WebGrind 分析,查看哪个函数/行消耗了大量内存。
Xdebug 的垃圾回收统计
xdebug_debug_zstd('your_var'); // 显示变量内存占用(需扩展)使用 memory_get_usage() / memory_get_peak_usage()
在代码关键位置打印内存,定位峰值点:
echo memory_get_usage() . " bytes\n"; // 执行一段代码 echo memory_get_peak_usage() . " bytes\n";Valgrind(用于 CLI 或常驻进程)
valgrind --tool=massif php script.php ms_print massif.out.<pid>PHP 扩展:memprof(Uber 出品)或 tideways
可生成内存调用栈火焰图。
3. 检查大变量
- 在怀疑的地方
var_dump($var)或使用serialize()后 strlen 估算。 - 注意:数组、对象、SQL 结果集、文件内容等容易占用大量内存。
4. 检查循环引用(PHP 5.3 之前)
- PHP 5.3 后引入同步垃圾回收机制,但长生命周期对象中仍然可能出现循环引用导致内存泄漏(在常驻进程中尤其明显)。
- 使用
gc_collect_cycles()强制回收,观察内存变化。
5. 监控常驻进程
- 对于 Swoole/Workerman,定期记录
memory_get_usage(),若持续增长说明有泄漏。 - 使用
top或htop观察进程 RSS 内存。
三、如何避免内存溢出?
1. 优化数据处理方式
(1)分批处理大数据集
// 错误:一次性加载所有数据到数组
$users = $db->query("SELECT * FROM users"); // 可能几百万行
// 正确:使用游标或分页
$stmt = $db->prepare("SELECT * FROM users LIMIT :offset, :limit");
for ($offset = 0; $offset < $total; $offset += 1000) {
// 处理 1000 条
}
(2)使用生成器(yield)
function getRows($file) {
$handle = fopen($file, 'r');
while (!feof($handle)) {
yield fgetcsv($handle);
}
fclose($handle);
}
foreach (getRows('huge.csv') as $row) {
// 每次只占用一行内存
}
2. 及时释放资源
- 对大变量
unset()或赋值为null(尤其循环中)。 - 关闭数据库连接、文件句柄、curl 资源。
3. 避免静态/全局变量积累
- 在常驻进程中,静态数组或全局缓存会无限增长,需设计合理的 LRU 或 TTL 清理机制。
4. 控制深递归
- 递归深度过大(如无限递归或极深树遍历)会耗尽调用栈内存。改用迭代。
5. 调整 memory_limit(治标不治本)
memory_limit = 512M # 根据实际需求调整,不要盲目增大
6. 使用引用传值减少拷贝
- 但注意引用可能导致意外共享,谨慎使用。
7. 启用 PHP 的垃圾回收
- 默认已启用,可通过
gc_enable()确认。 - 对于长期运行的脚本,定期调用
gc_collect_cycles()强制回收。
8. 利用外部存储
- 临时大量数据存放到 Redis、Memcached 或磁盘文件,而不是内存数组。
四、典型场景与解决方案
| 场景 | 导致内存溢出的原因 | 解决方案 |
|---|---|---|
| 处理百万级 Excel | PHPExcel 一次性加载所有行 | 改用 Spout 或分块读取、逐行写入 |
| 图片处理 | imagecreatefromjpeg 加载超大图片 | 限制上传尺寸、使用 imagescale 先缩放 |
| API 响应组装 | 嵌套数组/对象过多 | 使用 JsonSerializable 接口,延迟生成数据 |
| 常驻进程(Swoole) | 全局数组/静态属性不断追加 | 定期清理、使用 LRU Cache、重启进程 |
| 模板渲染 | 循环中拼接巨大字符串 | 改为 ob_start() + 分段输出 |
五、总结
- 排查:日志 +
memory_get_usage()定位峰值 + Xdebug/Valgrind 分析调用栈。 - 避免:分批处理、生成器、及时释放资源、限制递归、合理设计常驻进程数据结构。
- 原则:永远不要仅靠增大
memory_limit掩盖问题,要从算法和数据结构上降低内存占用。
内存溢出本质上是对资源的规划不足。遵循“按需加载、及时释放”的原则,就能让 PHP 脚本在有限内存下高效运行。