ZMM掩盖泄漏:PHP 8.5.5启用ZMM后,memory_get_usage(true)仅统计其内部分配,而ps RSS反映真实物理内存;两者长期脱节(如RSS持续涨、前者几乎不变)即表明ZMM层下存在泄漏,需用ps采样、-d zend_mm=0验证及gc_collect_cycles()配合debug_zval_dump排查循环引用与C资源未释放。

memory_get_usage() 和 RSS 不一致,说明 ZMM 在掩盖泄漏
PHP 8.5.5 默认启用 Zend Memory Manager(ZMM),memory_get_usage(true) 返回的是 ZMM 内部统计值,而 ps aux 看到的 RSS 是进程真实物理内存。两者长期脱节(比如 RSS 持续涨、memory_get_usage() 几乎不动),就是 ZMM 层下有泄漏。
- 用
ps -o pid,rss,comm -p <pid></pid>每 10 秒采样一次 FPM worker 或 Webman worker 的 RSS,连续 5 分钟;涨超 20MB 且不回落,基本坐实 - 临时验证:启动时加
-d zend_mm=0(仅调试),此时memory_get_usage(true)和 RSS 会趋近一致 -
memory_get_usage(true)必须放在脚本末尾调用才有效;中间或异常分支里取值容易误判
gc_collect_cycles() 返回 0,但对象没释放?检查循环引用和闭包捕获
PHP 8.5.5 的 GC 能处理多数循环引用,但前提是对象真正脱离作用域、且没有外部强引用残留。一旦闭包 use ($this) 或静态属性持有了对象,GC 就束手无策。
- 典型陷阱:
Event::on('xxx', function () use ($this) { ... })——$this被锁死,整个服务容器无法回收 - 协程路由中写匿名函数访问
$this->db或$this->container,同样钉住大对象 - DOMDocument、SimpleXML、PhpSpreadsheet 等库内部存在双向引用,
unset($dom)不够,必须显式调用$dom->clear()或$spreadsheet->disconnectWorksheets() - 用
debug_zval_dump($var)查refcount__gc和is_ref__gc,确认是否真无强引用
GD、cURL、PDOStatement 这些资源,unset() 完全不管用
这些由 C 扩展直接分配的资源,不在 Zend 内存管理器管辖范围,PHP 层的 GC 和析构函数根本触碰不到。漏掉显式销毁,RSS 就会稳步上涨。
-
imagedestroy($image)必须调用,unset($image)或对象析构都不触发释放 - cURL 句柄:必须配对
curl_init()和curl_close($ch);foreach ($pdo->query() as $row)后记得$stmt->closeCursor() - Redis 连接、Swoole
$server->connections映射表,不能当缓存键长期持有——连接断开后仍留着引用,就是泄漏源 - 注意:
__destruct()不保证执行;FPM worker 被 kill、超时退出、致命错误发生时,它会被跳过
ThinkPHP/Webman/Swoole 场景下,static 缓存和跨域共享是最大雷区
常驻进程框架里,static 数组生命周期 = worker 进程生命周期。往里面塞东西不清理,就是往内存黑洞里倒水。
立即学习“PHP免费学习笔记(深入)”;
- ThinkPHP 多应用未隔离:多个二级域名共用
runtime/container.php,导致单例重复注册、闭包捕获堆积 - Webman 中
self::$cache = []被反复[]=,又没 TTL 或clear(),压测 30 秒 RSS 翻五倍很常见 - 全局静态类滥用:
class LogHandler { public static $buffer = []; }—— 所有请求都往同一个数组追加 - 优先用
WeakMap替代普通数组缓存:private static $cache = new WeakMap();,对象销毁后条目自动消失
最易被忽略的是 JIT 缓冲区泄漏——opcache.jit_buffer_size 设得过大,高频递归或闭包触发 JIT 编译后,机器码内存不释放。CLI 或消息队列消费者这类长生命周期场景,必须关 JIT 或设小 buffer,不能只靠重启兜底。



















