内存溢出

一、什么是内存溢出

内存溢出(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(),若持续增长说明有泄漏。
  • 使用 tophtop 观察进程 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 或磁盘文件,而不是内存数组。

四、典型场景与解决方案

场景导致内存溢出的原因解决方案
处理百万级 ExcelPHPExcel 一次性加载所有行改用 Spout 或分块读取、逐行写入
图片处理imagecreatefromjpeg 加载超大图片限制上传尺寸、使用 imagescale 先缩放
API 响应组装嵌套数组/对象过多使用 JsonSerializable 接口,延迟生成数据
常驻进程(Swoole)全局数组/静态属性不断追加定期清理、使用 LRU Cache、重启进程
模板渲染循环中拼接巨大字符串改为 ob_start() + 分段输出

五、总结

  • 排查:日志 + memory_get_usage() 定位峰值 + Xdebug/Valgrind 分析调用栈。
  • 避免:分批处理、生成器、及时释放资源、限制递归、合理设计常驻进程数据结构。
  • 原则:永远不要仅靠增大 memory_limit 掩盖问题,要从算法和数据结构上降低内存占用。

内存溢出本质上是对资源的规划不足。遵循“按需加载、及时释放”的原则,就能让 PHP 脚本在有限内存下高效运行。