memory_get_usage(true) 才反映真实内存占用,false 仅统计 php 用户区未释放内存;泄露常表现为 true 持续上升而 false 波动小,因 zend 或扩展层(如 pdo、redis)长期持资源未触发 gc。

为什么 memory_get_usage() 看不出泄露,但进程内存却一直涨
因为 memory_get_usage(true) 和 memory_get_usage(false) 返回的不是同一类数据:false(默认)只算 PHP 用户区已分配但未释放的内存块,不包括底层 malloc 未归还给系统的部分;true 才是实际向操作系统申请的总内存。真正泄露时,常表现为 memory_get_usage(true) 持续上升,而 false 波动不大——说明 PHP 认为自己“没漏”,其实是 Zend 内存管理器或扩展层(如 PDO、Redis)长期持有资源没触发 GC。
- 线上排查务必用
memory_get_usage(true),否则大概率误判 - ThinkPHP 的
Db::table()->select()如果结果集极大且未及时 unset,会卡住引用计数,GC 不触发 - 自定义命令行任务里用
foreach ($query->cursor())比->select()更安全,但要注意 cursor 默认仍会缓存元信息
ThinkPHP 中哪些写法会绕过 GC 导致引用堆积
TP6/TP7 的容器、事件监听器、查询构造器都重度依赖闭包和对象引用,稍不注意就形成环形引用。最典型的是在控制器或中间件里把 $this 传进闭包,或者把 Db 查询结果赋给静态属性。
- 避免
static $cache = [];直接存Db::name('user')->find()返回的对象——Model 实例持有着 Query、Connection、甚至日志实例的引用链 - 不用
think\facade\Cache::remember()缓存大数组或对象,它底层用 serialize,反序列化后对象引用关系可能异常 - 在
__destruct()里手动清空$this->data、$this->relation等大字段,尤其 Model 继承自think\Model时
怎么用 gc_collect_cycles() 主动触发但又不拖慢请求
gc_collect_cycles() 不是“清理内存”,而是扫描并销毁环形引用。它本身有开销,高频调用反而降低吞吐。关键是在 GC 周期被抑制的场景下补一刀,比如长循环处理分页数据、或 CLI 脚本中批处理。
- Web 请求中不要在每个 action 结尾加
gc_collect_cycles();可在中间件里对特定路由(如导出接口)做一次:if (strpos($request->url(), '/export') === 0) { gc_collect_cycles(); } - CLI 脚本里每处理 100 条记录后调用一次,比每次循环都调用更合理
- 确认 GC 开关开着:
var_dump(gc_enabled());,某些 Docker 镜像或 SAPI(如 php-fpm 的某些配置)会默认关掉
用 xdebug_debug_zval() 查具体变量谁在 hold 引用
当怀疑某个变量没被释放,直接看它的引用计数和是否为“refcounted”。注意:这个函数只能在 CLI 或开发环境用,且需开启 Xdebug;生产环境请改用 debug_zval_dump()(功能弱但无依赖)。
- 别只看
refcount数字,重点看是否显示is_ref=1—— 表示被显式引用(&$a),这种最难被 GC 收走 - 在 Model 的
toArray()后立刻调用:xdebug_debug_zval('result');,常发现关联模型的$relation数组里存着未 unset 的原始 Query 对象 - TP 的
Collection类重写了__debugInfo(),所以var_dump($collection)看不到内部数据结构,必须用xdebug_debug_zval()看原始 zval
真正难处理的从来不是单个变量,而是 Model → Relation → Query → Connection → PDOStatement 这种跨组件的隐式引用链。盯住 Connection 实例的生命周期,比优化 SQL 更管用。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











