valgrind仅对c扩展、zend引擎或php-fpm有效,查php脚本层泄漏应优先用xdebug和memory_get_usage()+gc_collect_cycles();重点观察gc后内存是否回落及xdebug堆快照中的强引用链。

Valgrind 对 PHP 脚本层的内存泄漏完全无效,它只对 C 扩展、Zend 引擎或 php-fpm 本身有用;Xdebug 才是查 循环引用、static 变量堆积、闭包捕获 的正确工具。
Valgrind 报 definitely lost 但 stack trace 里没你的扩展名?那基本不是你的问题
Valgrind 输出中真正要盯的是 definitely lost 行和紧随其后的 stack trace。如果 trace 停在 zend_hash_add、emalloc、add_next_index_string 这类 Zend 内部函数,说明泄漏来自你调用了这些 API 却没配对释放(比如用 emalloc 分配但漏了 efree);如果 trace 出现在 curl_easy_init 或 PQconnectdb,则是第三方库资源未 cleanup。但若 trace 里全是 php_fpm、main、zif_ 开头的通用函数,且没出现你的扩展源文件路径(如 myext.c:247),大概率是 PHP 自身或其它已加载扩展的问题——别在这儿死磕。
memory_get_usage() + gc_collect_cycles() 是最轻量的脚本层泄漏定位法
不需要装任何扩展,直接在关键逻辑前后插入三行:
echo "before: " . memory_get_usage() . "\n"; // 你的业务代码 echo "after: " . memory_get_usage() . "\n"; unset($data, $obj); gc_collect_cycles(); echo "after gc: " . memory_get_usage() . "\n";
观察三次输出的差值。重点看「after」到「after gc」是否回落 —— 如果几乎不降,说明存在 GC 无法处理的强引用,比如:
- 闭包里通过
use ($obj)捕获了对象,且该闭包被注册为事件回调或存入全局数组 -
static $cache = [];不断往里塞数据,又没设淘汰策略 - 对象 A 持有对象 B 的引用,B 又反向持有 A,且两者都没被外部变量引用(典型循环引用)
Xdebug 堆快照必须配合 kcachegrind 或 qcachegrind 看引用链
仅生成 cachegrind.out.* 文件没用,得用图形化工具展开「Inclusive Heap Size」列,再点开可疑对象的「References」标签页:
- 找
refcount≥ 2 但生命周期已结束的对象 - 检查
type是object还是array,再看它的properties或elements是否包含指向自身的指针(如parent→children→parent) - 注意
Closure类型对象的this和use字段,它们常是隐藏引用源
别依赖 Xdebug 的「Memory Usage」汇总页——它只告诉你谁占得多,不告诉你为什么删不掉。
PHP 8.6 启用 zend_gc_debug 后输出的 GC STAT 日志容易被忽略
加这行配置后,每次 gc_collect_cycles() 会输出类似:
GC STAT: root buffer length=120, runs=45, collected=18, helpers=0
关键看 collected 值是否长期为 0,或远低于 runs。如果连续几次 collected=0,说明 GC 根本没触发清理,大概率是对象被外部强引用钉住(比如注册到了 swoole_server 的回调表、Laravel 的 Container、或自定义的全局事件总线)。这时候得顺着引用链往上翻,而不是怪 GC 失效。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











