内存缓慢上涨基本可排除纯内存碎片,应优先排查隐藏强引用;用memory_get_peak_usage(true)定时打点确认泄漏趋势,若每分钟稳定上涨超1–2mb且不回落,即为典型泄漏信号。

不是 GC 没触发,而是强引用没断开 —— 内存缓慢上涨基本可以排除纯内存碎片,优先查隐藏引用持有者。
用 memory_get_peak_usage(true) + 定时打点确认泄漏趋势
只看 memory_get_usage() 容易误判,它反映的是当前脚本分配的内存块,但 Zend 内存池会缓存小内存;memory_get_peak_usage(true) 才是真实向系统申请的峰值,更可靠。
- 在 Swoole 的
onReceive、onMessage或任务处理入口/出口处插入打点,例如:echo sprintf("[MEM] %s | Peak: %.2fMB\n", date('H:i:s'), memory_get_peak_usage(true) / 1024 / 1024); - 配合
Swoole\Timer::tick(30000, ...)每 30 秒输出一次,持续观察 10–20 分钟:如果每分钟稳定上涨 >1–2MB,且不回落,就是典型泄漏信号 - 注意区分「单次请求涨」和「跨请求累积涨」——前者可能是大数组没 unset,后者才是真正的长驻泄漏
用 gc_collect_cycles() + gc_mem_caches() 看是否“假涨”
PHP 8.1 CLI 模式下,小内存(
- 在每次任务结束时主动调用:
gc_collect_cycles();<br>gc_mem_caches();
(gc_mem_caches()是 PHP 7.4+ 引入、8.0+ 稳定的函数,专清小内存缓存) - 如果调用后
ps aux --sort=-%mem | grep php显示的 RSS 明显下降(比如从 320MB → 260MB),说明是碎片问题,不是对象泄漏 - 若 RSS 几乎不变,说明有强引用钉住了对象,GC 根本没机会回收 —— 这时才要深入查引用链
用 Xdebug 堆快照定位谁持有谁
仅靠 debug_zval_dump() 不够,它只显示当前变量的 refcount,看不出跨作用域的间接引用。必须用 Xdebug 生成堆快照(heap snapshot),再用 QCacheGrind 或 PhpStorm 打开分析。
- 启用方式(CLI 下):
php -d xdebug.mode=develop,profile -d xdebug.start_with_request=yes -d xdebug.output_dir=/tmp script.php
(生成/tmp/cachegrind.out.*) - 关键技巧:在疑似泄漏前(如第 1 轮处理完)和泄漏后(如第 50 轮)各抓一个快照,用工具对比「新增存活对象数」和「retained size」最大的类
- 重点关注:
stdClass、ArrayObject、ORM 实体、闭包(Closure)、Swoole\Coroutine对象 —— 它们常是泄漏源头 - 避坑:Xdebug 默认不记录闭包 use 的变量,需加
-d xdebug.collect_params=4才能看清是不是某个大$this被捕获了
重点排查这几类强引用陷阱
90% 的长驻泄漏来自这几处,它们看起来合法,但生命周期远超预期:
- 事件监听器注册后没注销:
$server->on('message', $callback)中的$callback是闭包,若它 use 了$container或$db,整个服务容器就钉住了 -
foreach (&$item)循环后没unset($item):最后一个元素的引用会残留,导致整个数组无法释放 - 静态属性缓存无 TTL 或清理机制:
static $cache = [];在协程中不断$cache[$key] = $bigObj;,却不设上限或定时 flush - PDOStatement 或 Redis 连接未显式
close():某些扩展在 C 层未释放底层资源,zval 虽销毁,fd 和内存仍卡住 - 协程内创建的
WeakReference被误当成强引用使用:比如$wr->get()后赋给全局变量,等于又持有了强引用
最麻烦的是循环引用藏在框架层 —— 比如 Laravel Octane 的 Application 被闭包捕获,又通过 Container 持有所有已解析服务,一个请求就能带出几百 MB。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











