根本原因是frankenphp worker模式拉长请求生命周期,导致symfony的entitymanager一级缓存不清理、闭包监听器强引用容器、opcache/twig缓存膨胀、db连接与文件句柄未显式关闭等隐患持续累积。

根本原因不是 PHP 内存泄漏本身,而是 FrankenPHP Worker 模式下“请求生命周期被拉长”,导致 Symfony(或 Laravel)原本在单次请求结束时自动销毁的对象、缓存、连接全堆在一个常驻进程里不释放。
EntityManager 一级缓存持续累积
Symfony 的 EntityManager 默认开启一级缓存(identity map),所有 find() 或 getRepository()->findOneBy() 加载的实体都会被强引用保留在内存中。Worker 进程不重启,$em->clear() 不调用,实体对象就只增不减。
- 批量操作(如导出、同步)后必须手动加
$this->em->clear(),不是close() - 避免在循环里反复
new Entity()后不persist()也不clear(),这是最典型的内存爬升场景 - 检查
doctrine.orm.auto_generate_proxy_classes是否为false(生产环境必须关),否则每次新实体类都会生成并加载代理类,吃内存
闭包监听器 + 容器引用形成强循环
用 function () use ($container) { ... } 或 function () use ($bigService) { ... } 注册事件监听器,会导致整个 $container 被闭包捕获——即使你 later 调用 $dispatcher->removeListener(),PHP GC 也无法回收,因为闭包仍持有对大对象的强引用。
- 改用
[$this, 'onSomething']或static function ($event)形式注册监听器 - 禁用开发专用监听器:确保
symfony/debug-pack和profiler-pack不在生产 autoload 中加载(从require-dev移出或用autoload-dev条件排除) - 在
kernel.terminate事件里主动清空临时大数组:$this->tempResults = [];
OPcache + Twig 缓存跨请求膨胀
FrankenPHP 是 CLI 模式启动,但默认会启用 opcache.enable_cli=1,导致 OPcache 把所有已加载类永久锁住;Twig 模板编译缓存若路径未固定或权限不对,也会每次请求重新编译,缓存文件越积越多。
- 确认
php.ini中已设opcache.enable_cli=0(关键!CLI 下 OPcache 应关闭) - 检查
opcache_get_status()['opcache']['oom_restarts']是否非零,是则说明 OPcache 频繁因内存不足重置 - 确保
var/cache/prod目录由运行 FrankenPHP 的用户(如www-data)完全可写,且路径不带realpath()或__DIR__动态拼接 - 在控制器中打点:
error_log('Twig cache: ' . count(glob('var/cache/prod/twig/*')));,观察是否随请求数线性增长
数据库连接与文件句柄未显式关闭
Worker 进程不会像 FPM 那样在请求结束时自动关闭 PDO 连接或 fclose() 文件句柄。不手动清理,连接池耗尽、文件描述符打满、内存缓慢上涨都是必然结果。
- 所有 PDO 实例处理完后必须赋值为
null:$pdo = null;(仅unset()不够) - MySQLi 必须调用
$mysqli->close(),不能只unset($mysqli) - 用
fopen()打开的文件,必须配对fclose();避免在循环中反复fopen()不关 - ORM 层如 Laravel 的
DB::purge()或 Doctrine 的$connection->close()要在请求末尾显式触发
真正危险的不是某次请求爆内存,而是内存缓慢爬升到某个阈值后触发 OOM Killer —— 这时连日志都来不及写。所以别等报错才查,要在 max_requests 触发前,用 memory_get_peak_usage(true) 打点采样,看趋势是否阶梯上升。一旦发现,立刻定位是哪类对象没释放,而不是笼统调大 memory_limit。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











