协程内存泄漏主因是未清理引用,包括闭包强引用对象、static/global 数据未清、Swoole\Table/Redis无上限缓存、onClose未触发致僵尸协程、定时器未清除及GC无法覆盖跨协程引用;需显式释放、设限、配过期、绑定会话超时驱逐、defer清理资源并监控堆快照。

协程退出时没清理引用,内存就卡住了
协程不会自动清掉它用过的变量,尤其当闭包、静态属性或全局数组里存了对象时,GC 看不到“没人用了”,就一直占着内存。最典型的是 go(function () use ($obj) { ... }) 这种写法——$obj 被闭包强引用,哪怕协程执行完,只要闭包还存在,$obj 就不会释放。
实操建议:
- 显式
unset($obj)或把大对象改成传参而非use:比如go(function ($data) { ... }, $largeArray) - 用
WeakReference::create($obj)替代直接use,避免闭包拖住对象生命周期 - 协程内避免给
static或global写入未清理的数据,例如static $cache = [];每次都$cache[$key] = $value却不设 TTL 或清理逻辑
Swoole\Table / Redis 缓存没设上限,越攒越多
Swoole\Table 和 Redis 作为协程间共享数据的常用载体,很容易变成内存泄漏的“温床”。比如用 $table->set($key, ['data' => $bigObj]) 存了大数组,但没配 size 限制或没做淘汰策略,表越写越大;Redis 里用 hSet('session:'.$id, 'context', serialize($obj)) 存协程上下文,却忘了配过期时间。
实操建议:
-
Swoole\Table初始化时必须指定size,并监控$table->count(),超阈值主动del旧条目 - 所有 Redis 写入必须带
EX或PX:如$redis->hSetEx('ctx:'.$cid, 300, 'data', json_encode($ctx)) - 避免在
Table或 Redis 中存 PHP 对象实例(如new StdClass()),只存序列化数组或字符串
onClose 回调没触发,协程就“假死”挂着
Nginx 反向代理断连、客户端网络闪断、心跳超时未响应……这些场景下,$server->on('close') 可能根本不会执行,导致协程里分配的资源(undo buffer、锁、连接池句柄)没人收尾。协程本身也没退出,变成“僵尸协程”,持续吃内存。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
实操建议:
- 为每个协程绑定唯一
$sessionId,注册到Swoole\Table或 Redis,并设置定时器每 30 秒扫描一次:若last_heartbeat < time() - 45,则co::cancel($cid)并手动清 context - 服务端主动设
$server->set(['heartbeat_idle_time' => 45, 'heartbeat_check_interval' => 15]),靠心跳保活+超时驱逐 - 所有资源获取(DB 连接、文件句柄、锁)必须配
defer或try/finally,确保无论是否走到onClose都能释放
GC 不是万能的,得帮它一把
PHP 的 GC 能处理循环引用,但对跨协程、跨请求的长生命周期引用无能为力。比如一个协程启动了定时器:Swoole\Timer::tick(60000, function () { ... }),回调里又用了 $this 或闭包捕获了外部对象——这个定时器不手动 clear,它就永远活着,拖着所有引用的对象。
实操建议:
- 每次
Swoole\Timer::tick()或after()都要保存返回的$timerId,在协程退出前调用Swoole\Timer::clear($timerId) - 高频调用
gc_collect_cycles()+gc_mem_caches(),尤其在长任务分段点(如每处理 100 条消息后) - 上线前用
meminfo扩展 dump 堆快照,对比两次请求间的stdClass、array实例数增长,快速定位谁在“囤货”
协程内存泄漏不是某个函数写错了,而是整个资源生命周期管理链条上任何一个环节松动,都会让内存慢慢淤积。最容易被忽略的,是那些“看起来很安全”的地方:一次没清的 Table 条目、一个忘了 cancel 的定时器、一段以为会被自动回收的闭包——它们叠加起来,就是 OOM 的前奏。

















