本文目录导读:

PHP 内存泄漏排查是一个系统性的过程,需要结合工具、代码审查和监控,以下是一套完整的排查指南:
理解 PHP 内存模型
首先明确:PHP 的“内存泄漏”通常是“内存峰值过高”,因为 PHP-FPM 每个请求结束后会释放大部分内存(进程回收),真正的问题往往出现在:
- 长驻进程(如 swoole、workerman、队列消费者)
- 单次请求处理数据量过大
- 循环引用未清理
- 全局变量/静态变量累积
定位泄漏的 6 个步骤
步骤1:确认是否存在泄漏
# 监控单进程内存(持续观察)
while true; do ps -o rss,pmem,cmd -p $(pgrep -f "php-fpm: pool" | head -1); sleep 5; done
# 批量查看所有 PHP 进程内存
ps aux | grep php | awk '{print $2, $6/1024"MB", $11}'
步骤2:启用内存检测工具
Xdebug + 内存分析:
// 在脚本中分段记录 $usage = memory_get_usage(); // ...业务代码... echo "操作A后: " . (memory_get_usage() - $usage) . " bytes\n";
使用 PHP 内置函数定位:
class MemoryTracker {
private static $markers = [];
public static function mark($label) {
self::$markers[$label] = [
'usage' => memory_get_usage(true),
'peak' => memory_get_peak_usage(true),
'time' => microtime(true)
];
}
public static function report() {
$prev = null;
foreach (self::$markers as $label => $data) {
$diff = $prev ? $data['usage'] - $prev['usage'] : 0;
echo "$label: 使用{$data['usage']} bytes, 增长{$diff} bytes\n";
$prev = $data;
}
}
}
步骤3:使用 Valgrind 定位 C 层面泄漏
# 安装
apt-get install valgrind
# 运行 PHP 脚本(需编译 PHP 时支持)
valgrind --tool=memcheck --leak-check=full \
--show-leak-kinds=all \
--log-file=valgrind.log \
php your_script.php
步骤4:检查 PHP 扩展泄漏
# 逐个禁用扩展测试 php -d extension_dir=/usr/lib/php/20210902 -n your_script.php # 通过 php-fpm 日志确认 php-fpm -t 2>&1 | grep -i memory
步骤5:代码审查重点区域
// 高危模式1:静态/全局变量累积
class Cache {
private static $items = []; // 永不释放
public static function set($key, $value) {
self::$items[$key] = $value; // 如果无限增长,就是泄漏
}
}
// 高危模式2:大数据量循环处理
$data = DB::select("SELECT * FROM huge_table");
foreach ($data as $row) {
// 处理但未清除引用
}
// 高危模式3:闭包捕获
$result = [];
for ($i = 0; $i < 100000; $i++) {
$closure = function() use ($result) { // 捕获整个 $result
// 逻辑...
};
$result[] = $closure; // 每个闭包都带一份 $result 引用
}
// 高危模式4:长连接未复用
foreach ($items as $item) {
$conn = new PDO(...); // 每次新建连接
// 用完未关闭/释放
}
步骤6:专门工具分析
Blackfire.io(可视化分析)
# 安装黑火监控
# 在代码中添加埋点
blackfire_set_transaction_name("check_memory");
// 业务代码...
blackfire_end_transaction();
Tideways(生产环境分析)
自定义简单监控脚本:
// 每 100 次迭代检查一次
$startMemory = memory_get_usage();
for ($i = 0; $i < 1000000; $i++) {
processItem($i);
if ($i % 1000 == 0) {
$current = memory_get_usage();
$diff = $current - $startMemory;
if ($diff > 10 * 1024 * 1024) { // 超过 10MB
echo "可能的泄漏在迭代 $i: {$diff} bytes\n";
debug_print_backtrace();
}
}
}
常见泄漏场景与修复方案
场景1:循环引用导致泄漏
// 问题代码
class A { public $b; }
class B { public $a; }
$a = new A();
$b = new B();
$a->b = $b;
$b->a = $a; // 循环引用
// 修复:使用弱引用(PHP 7.4+)
$a->b = WeakReference::create($b);
场景2:Generator/Iterator 未销毁
// 问题代码
$generator = getHugeData(); // 返回 generator
foreach ($generator as $item) {
// 中途跳出循环
if ($condition) break; // generator 被保留,资源未释放
}
// 修复:手动调用 close
finally {
$generator->close();
}
场景3:文件/资源句柄未关闭
// 问题代码
foreach ($files as $file) {
$handle = fopen($file);
// 忘记 fclose()
}
// 修复:使用 try-finally 或 using 块
foreach ($files as $file) {
$handle = fopen($file);
try {
// 处理逻辑
} finally {
fclose($handle);
}
// 或者使用自动关闭的包装器
$content = file_get_contents($file); // 自动关闭
}
生产环境监控方案
添加内存监控指标到应用
// 在应用框架的 after-middleware/事件中
class MemoryMonitor {
public function __construct() {
$this->baseline = memory_get_usage();
}
public function __destruct() {
$used = memory_get_peak_usage() - $this->baseline;
if ($used > 50 * 1024 * 1024) { // 50MB 告警
error_log("高内存使用: " . formatBytes($used));
}
}
}
使用云监控工具
# Prometheus + Grafana 配置示例
php_memory_usage_bytes{process="php-fpm"}
php_memory_peak_usage_bytes{process="php-fpm"}
预防泄漏的最佳实践
- 使用依赖注入容器而不是全局/静态实例
- 避免循环引用:序列化数组时使用
serialize/unserialize断开引用 - 大数据量操作分页:不要一次加载全部数据
- 定期重启长驻进程:设置合适的内存上限自动重启
- 代码规范检查:使用 PHPStan/Psalm 静态分析工具
快速诊断命令汇总
# 快速查看最大内存占用的 PHP 进程
ps aux --sort=-rss | grep php | head
# PHP 脚本中实时监控
php -r 'echo ini_get("memory_limit");'
# 查看是否有未释放的 MySQL/Redis 连接
ss -tlnp | grep php-fpm
# 使用 PHP7+ 的内置检测
php -d display_errors=1 -d error_reporting=E_ALL -d opcache.enable=0 -d `php -d opcache.enable_cli=1 -d opcache.validate_timestamps=0` -r 'echo memory_get_usage();'
总结排查流程
确认内存持续增长(观察多次请求/循环内存变化)
2. 使用 memory_get_usage 分段定位(二分法找泄漏点)
3. 结合 Valgrind 排除扩展问题
4. 代码审查 + xdebug profile 分析
5. 实施修复并验证(监控回归)
关键点:先确认是不是真泄漏,再定位泄漏位置,最后修复和验证,工具只是辅助,最重要的还是理解代码中对内存引用的生命周期管理。