Webman内存泄漏本质是“长生命周期数组无限追加却不清理”,需同时满足静态/单例数组存在且持续写入无清理;确认真泄漏须压测百万请求后RSS仍线性上涨,排除xdebug、大文件处理等假性溢出,通过php start.php status观察Summary内存趋势及memory_get_peak_usage(true)差值判断。

Webman内存泄漏不是“配置调小就崩”,而是“静态数组越写越多却不清理”——只要满足“长生命周期 + 无限追加”两个条件,self::$data[] = $x 写一百万次,进程 RSS 就涨一百万份。
怎么确认是真泄漏,不是假报警
别一看到内存涨到 200M 就慌。Webman 常驻进程下,内存稳定在 80–120M 完全正常;处理大文件、查千万级数据后内存卡在 180M 不回落,也大概率只是 PHP 内存池复用,不是泄漏。
- 用
php start.php status查 Summary 字段的内存值,压测前后对比:30 秒 100 并发,内存增长 20M 且持续线性上升 → 需排查 - 关掉
xdebug:执行php -d zend_extension= -f app/Http/Controller/TestController.php看是否还报Allowed memory size of XXX bytes exhausted - 检查是否在中间件或监听器里写了
static $log = [];或protected static $cache = [];,又没做任何清理 - 运行
memory_get_peak_usage(true)(注意带true)在请求入口和出口各打一次点,差值稳定在几 KB 内 → 无泄漏;每轮请求固定 +12KB → 很可能有裸数组膨胀
为什么 ini_set('memory_limit', '1G') 没用
因为 Webman 启动后走的是 CLI 模式,php.ini 里的 memory_limit 在 worker 进程启动时已加载完毕,运行时 ini_set() 失效——它只影响当前脚本后续分配,不重置已分配内存,也不改变 ZendMM 的初始池大小。
- 真正生效的位置只有三个:
start.php最顶部(早于require和自动加载)、PHP-FPM pool 的www.conf中php_admin_value[memory_limit]、CLI 启动时加-d memory_limit=512M - 设成
-1(无限制)等于掩盖问题:进程 RSS 涨到 2GB 才 OOM,但你早就收不到 monitor 进程的自动重启告警了 -
monitor进程靠的是memory_get_usage(true)+ 阈值判断,不是看系统 RSS;所以即使你把memory_limit调高,monitor 仍会在 90% 阈值触发软重启 —— 这是保护机制,不是 bug
如何定位泄漏源:从 static 到 WeakMap
90% 的 Webman 内存泄漏源头就三类:static 数组、单例属性、闭包捕获 $this 或连接对象。重点不是“有没有引用”,而是“引用是否该被释放却没释放”。
立即学习“PHP免费学习笔记(深入)”;
- 全局搜
static \$.*= \[、protected static \$.*= \[、public static \$.*= \[,尤其盯紧app/Listener/和app/Middleware/下的类 - 发现
self::$items[] = $request->input();?立刻改:加长度控制if (count(self::$items) > 1000) array_shift(self::$items); - 若必须缓存对象(如用户连接映射),PHP 8.0+ 改用
new WeakMap()存储,避免强引用钉住整个对象树 - 闭包注册在
Worker::onMessage或定时器里?检查是否用了use ($connection, $this)—— 改成use ($connection)并在onClose中显式置空$connection->context = null
别信“GC 会自动回收”,要主动干预
PHP 的 GC 能处理简单循环引用,但在 Webman 长驻场景下,对象生命周期跨成百上千次请求,GC 触发时机不可控;等它回收,RSS 早就涨上去了。
- 在请求结束回调(如中间件
handle()最后)加gc_collect_cycles();,再调gc_mem_caches();清理 ZendMM 缓存池 - 不要依赖
__destruct():常驻进程里对象销毁时机模糊,unset()后立即gc_collect_cycles()更可靠 - Redis/MySQL 连接别在
onWorkerStart全局 new 一个复用到底;改用懒加载 + 健康检测:if (!$this->redis || !$this->redis->isConnected()) { $this->redis = new Redis(); } - 第三方 SDK(如 EasyWeChat)务必查文档:是否要求手动调
close()或destroy();没调,句柄和内部缓存就一直占着内存
最易被忽略的一点:monitor 进程的阈值判断基于 memory_get_usage(true),但它不感知 RSS 真实占用。有些泄漏(比如 cURL 句柄未关闭、扩展层 C 内存未 efree())会导致 RSS 暴涨而 memory_get_usage() 几乎不变 —— 这时候得用 ps aux --sort=-%mem | head -n 5 直接看系统级内存,再结合 lsof -p PID | wc -l 查打开文件数是否异常。



















