Webman内存持续上涨是常驻进程特性所致,非bug;真泄漏需满足static/闭包/单例无限累积且RSS不回落,须通过gc_collect_cycles()验证并针对性清理。

Webman 内存持续上涨不是 bug,而是常驻进程的默认状态;不主动清理 static、闭包、单例里的引用,内存就永远不会回落。
为什么 Webman 的 memory_get_usage() 会越跑越高
PHP-FPM 每次请求结束自动重置内存,Webman 不会。Worker 进程启动后一直活着,所有 static 变量、全局数组、闭包 use 的对象、未释放的数据库连接句柄,全被钉在内存里。
- 常见假性泄漏:开了
xdebug、var_dump后没删、用file_get_contents()加载大文件后没unset($content) - 真泄漏信号:压测 10 万次同一接口,
memory_get_peak_usage(true)每次都比前一次高 2KB+,且gc_collect_cycles()后ps aux看到的 RSS(常驻集大小)不降 - 关键区别:
memory_get_usage()只看 PHP 堆,ps aux的 RSS 才反映真实内存占用——后者不回落,才是要动手的地方
static 数组和单例缓存是内存黑洞的主力
写一次 self::$log[] = $item 很简单,但这个数组生命周期等于 Worker 进程寿命。没有清理逻辑,它只会无限膨胀。
- 错误模式:
Cache::set('user_123', $bigArray)却没配 TTL,也没调Cache::forget()或定时清理 - 更隐蔽的:
protected static $instances = [];在中间件里反复self::$instances[$key] = new HeavyService() - 正确做法:改用带容量上限的循环队列,例如
count(self::$cache) > 1000 && array_shift(self::$cache);或直接切到 Redis 缓存,交由外部管理生命周期 - 临时验证:在中间件末尾加
self::$temp = [];或unset($GLOBALS['debug_cache']),观察 RSS 是否回落
闭包和定时器偷偷锁住 Request/Response 实例
use ($request) 看似只是读个 $request->uri(),实际把整个 Request 对象(含原始 body、上传文件、解析后的 JSON)全拖进闭包作用域,GC 无法回收。
立即学习“PHP免费学习笔记(深入)”;
- 高频出事场景:
Timer::tick(60, function () use ($request) { ... })、Event::on('xxx', function () use ($this) { ... }) - 安全替代:
use ($request->id)或use ($request->get('uid')),只捕获 ID 类轻量数据;需要对象时,延迟查库而非捕获实例 - 协程环境下更危险:闭包内访问
$this->container或$this->db,会导致服务容器被强引用钉死 - 检测方法:在
onClose或定时器回调末尾加gc_collect_cycles(); echo memory_get_peak_usage(true), "\n";,对比是否回落
第三方 SDK 和连接池资源不释放会隐式驻留内存
很多 SDK(如 EasyWeChat、阿里云 OSS、Redis 客户端)在初始化时创建长生命周期资源(HTTP 连接池、Socket 句柄、内部定时器),若没显式关闭,这些 C 层资源不会随 PHP 对象销毁而释放。
- 典型错误:在
onWorkerStart里new Redis()全局复用,但没做健康检测或断线重连,连接句柄越积越多 - 正确姿势:懒加载 + 健康检查,例如
if (!$this->redis || !$this->redis->isConnected()) { $this->redis = new Redis(); } - SDK 必查项:翻文档确认是否有
close()、disconnect()、destroy()方法,并在onWorkerStop或连接关闭前调用 - 连接池配置陷阱:用
webman/redis扩展时,必须设max_idle_time和max_wait_time,否则空闲连接永不归还
最易被忽略的一点:内存泄漏往往不出现在业务主干,而藏在监听器、定时器、WebSocket 回调、甚至中间件的异常分支里。压测时别只盯首页,要对每个事件注册点做独立触发 + 内存快照比对。



















