FrankenPHP worker模式内存泄漏本质是常驻进程下对象未释放、引用链未断、资源未归还;90%以上源于闭包捕获$this、静态缓存无清理、第三方SDK未close及GC未触发,memory_get_usage()稳定不等于无泄漏。

FrankenPHP 1.3 在 worker 模式下出现内存泄漏,本质仍是 PHP 常驻进程场景下的典型内存管理问题——对象未释放、引用链未断、资源未归还。它虽基于 Swoole(或自研协程运行时),但内存行为逻辑与 Workerman/Swoole CLI 进程高度一致。排查不能只看 memory_get_usage(),而要穿透到“逻辑死亡却物理存活”的对象层。
重点先说结论:FrankenPHP worker 模式内存持续上涨,90% 以上源于闭包捕获 $this、静态缓存无清理、第三方 SDK 未 close、以及 GC 未及时触发;memory_get_usage() 稳定 ≠ 内存没涨,只是僵尸对象被强引用锁住,GC 没扫到。
? 看得见的内存增长:用对指标打点
memory_get_usage() 容易误判,它只反映当前脚本堆内存分配,不统计被闭包/静态属性隐式持有的“已死对象”。真正有效的是:
-
memory_get_peak_usage(true):记录每个请求/定时周期后的峰值内存,连续打点(如每 100 次请求记一次),观察是否线性上升 -
/proc/<pid>/status中的RSS(实际物理内存)和USS(独占内存):用smem -p -c "pid uss rss"对比,若 RSS 高但 USS 极低,大概率是共享内存(如 APCu、Swoole\Table)或内存碎片问题 -
ps aux --sort=-%mem | head -5:确认是哪个 worker 进程在涨,再针对性 attach 或日志采样
✅ 建议在
onRequest入口和出口处各打一次memory_get_peak_usage(true),差值超过 512KB 就值得深挖。立即学习“PHP免费学习笔记(深入)”;
? 最常出问题的三类代码结构
1. 闭包 + $this 形成的强引用闭环
比如在路由处理中写:
$this->app->get('/api', function () {
return $this->doSomething(); // 隐式捕获 $this
});这个闭包会持有一个对当前实例的强引用,而该实例又可能持有 static::$cache、$this->redis 等,形成闭环。GC 默认不回收。
✅ 正确做法:
- 回调尽量用静态方法或独立函数
- 必须用实例方法时,改用
[$class, 'method']数组语法(比匿名函数更轻量,但仍需警惕) - 禁止在闭包里
use ($this)—— PHP 不允许,但use ($largeObj)同样危险
2. 静态缓存无限累积,且 key 是对象本身
常见错误:
class ApiHandler {
public static $cache = [];
public function onRequest($req) {
self::$cache[$req] = $this->process($req); // ❌ $req 是 Request 对象,强引用
}
}$req 对象一旦被其他地方(如中间件、日志器)引用,整个 self::$cache 就无法释放。
✅ 正确做法:
- 键必须是标量:
self::$cache[$req->id()]或md5($req->uri()) - PHP 8.0+ 强烈改用
WeakMap:private static WeakMap $cache; public function onRequest($req) { self::$cache[$req] = $this->process($req); // 连接断开后自动解绑 }
3. 第三方 SDK / 资源未显式释放
FrankenPHP worker 生命周期长,new Redis()、new PDO()、EasyWeChat 实例、HTTP 客户端等,若只 new 不 close() 或 unset(),连接句柄和内部回调定时器会持续占内存。
✅ 必须做到:
- 所有资源在
onRequest结束前检查并释放:if ($this->redis && $this->redis->isConnected()) { $this->redis->close(); } unset($this->redis); - 使用
try ... finally包裹关键资源操作 - 数据库连接优先走连接池(如
spiral/database),避免每次 new
?️ 工具链精准定位泄漏源
1. xdebug_debug_zval() 查引用链
开启 Xdebug(xdebug.mode=develop),在疑似泄漏对象后插入:
xdebug_debug_zval('self::$cache');观察输出中的 refcount 和 is_ref。若 refcount > 1 且你确认只该处使用,说明被意外持有——顺着 refcount 链往上找是谁加的引用。
2. gc_collect_cycles() 主动触发回收 + 对比
不要等 GC 自动运行。在每次请求尾部加:
gc_collect_cycles(); // 再打一次 memory_get_peak_usage(true)
如果加了之后内存回落明显,说明确实是循环引用未清;如果没变化,就要查 C 扩展或内存碎片。
3. gc_mem_caches() 清理小内存碎片(PHP 8.1+)
FrankenPHP 常跑高版本 PHP,若 RSS 持续涨但 USS 很低,很可能是小对象内存碎片:
// 每 5 分钟执行一次 Timer::add(300, fn() => gc_mem_caches());
这个函数专门释放 Zend 内部的小内存缓存块(<3KB),不归还给 OS,但能降低 RSS 增速。
⚙️ FrankenPHP 特有配置检查项
- 确认没在启动脚本中调用
gc_disable()(极少数模板项目会误关) - 检查
frankenphp_worker_max_requests是否设得太小(如 100),导致频繁重启掩盖泄漏;设为0(不限制)便于观察真实泄漏趋势 - 若启用了
apcu或opcache,确认apc.enable_cli=1且opcache.memory_consumption不过大;APCu 缓存也需手动apcu_clear_cache()定期清理 - 日志中搜索
Allowed memory size exhausted—— 若报错但memory_limit设得很高(如 512M),基本可判定是泄漏而非配置不足
不复杂但容易忽略。



















