在一次线上订单高峰中,某电商平台的结算脚本在处理完几千笔数据后莫名其妙地崩溃,日志里只留下了“Allowed memory size exhausted”的字样。追根溯源,问题并非数据量本身,而是循环中未及时释放的对象引用让 PHP 持续占用堆内存,最终形成了所谓的内存泄漏。
内存泄漏的常见根源
PHP 虽然有垃圾回收机制,但在以下几种情形下,它会失效或被“骗”走:
- 闭包捕获了外部大对象,却在循环外部仍保持引用。
- 静态属性或单例模式中存放了临时数据,却忘记在业务结束后清空。
- 使用
mysqli_fetch_all()一次性读取全表,结果数组占据数百 MB。 - 图片处理库(如 GD)在循环里创建大量
imagecreatefromjpeg(),未调用imagedestroy()释放资源。
防御性编程技巧
针对上述陷阱,业界常用的“硬核”做法如下:
// 使用生成器逐行读取,避免一次性占满内存
function streamRows(PDO $pdo, string $sql): Generator {
$stmt = $pdo->query($sql);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
}
// 示例:遍历 10 万条订单记录
foreach (streamRows($pdo, 'SELECT * FROM orders') as $order) {
// 业务处理
}
闭包里若不需要外部变量,务必使用 use () 空列表,防止意外捕获;单例或缓存类在 reset() 方法中主动 unset() 临时属性;图片处理完毕后统一调用 imagedestroy(),甚至可以把 gc_collect_cycles() 放在批次结束处强制回收。
“内存泄漏往往是细节的堆叠,及时审视每一次对象的生命周期,是把握性能的关键。”
把这些守则写进代码审查清单,配合 CI 的内存基准测试,几乎可以把突如其来的 OOM 错误拦在门外。只要在开发初期就养成“用完即走”的思维,后期的扩容压力自然会大幅下降。
