php方案 PHP的逃逸分析
·
先说概念:
逃逸 = 变量活过了它被创建的那个函数
不逃逸 = 变量在函数结束时就死了
逃逸的变量 GC 要追踪更久,内存压力更大
不逃逸的可以提前回收
composer require nikic/php-parser
<?php
require 'vendor/autoload.php';
use PhpParser\NodeFinder;
use PhpParser\ParserFactory;
use PhpParser\Node;
$code = <<<'PHP'
<?php
function process() {
$local = new stdClass();
$escaped = new stdClass();
$fn = function() use ($local) { return $local; };
return $escaped;
}
PHP;
$ast = (new ParserFactory)->createForNewestSupportedVersion()->parse($code);
$finder = new NodeFinder;
foreach ($finder->findInstanceOf($ast, Node\Stmt\Return_::class) as $ret) {
if ($ret->expr instanceof Node\Expr\Variable) {
echo "return 逃逸: \${$ret->expr->name}\n";
}
}
foreach ($finder->findInstanceOf($ast, Node\Expr\ClosureUse::class) as $use) {
echo "闭包捕获逃逸: \${$use->var->name}\n";
}
输出:
return 逃逸: $escaped
闭包捕获逃逸: $local
---
逐行解释:
$code = <<<'PHP' ... PHP;
把要分析的 PHP 代码当字符串传进来,这里故意写了两种逃逸:return 带出去、闭包捕获。
---
$ast = (new ParserFactory)->createForNewestSupportedVersion()->parse($code);
把代码字符串解析成 AST(抽象语法树),就是把代码变成一棵可以遍历的树形结构,每个节点代表一个语法元素。
---
$finder = new NodeFinder;
AST 遍历工具,用来在树里找特定类型的节点,不用自己写递归。
---
$finder->findInstanceOf($ast, Node\Stmt\Return_::class)
在整棵树里找所有 return 语句节点,返回一个数组。
---
if ($ret->expr instanceof Node\Expr\Variable) {
echo "return 逃逸: \${$ret->expr->name}\n";
}
检查 return 的是不是一个变量(不是字面量、不是表达式),是的话就说明这个变量逃逸了,打出变量名。
---
$finder->findInstanceOf($ast, Node\Expr\ClosureUse::class)
找所有闭包的 use(...) 捕获列表,use ($local) 里的 $local 就是一个 ClosureUse 节点。
---
echo "闭包捕获逃逸: \${$use->var->name}\n";
被闭包捕获的变量也算逃逸,因为闭包可能活得比原函数更久,变量不能提前释放。
---
两种逃逸对比:
$escaped → return 带出函数 → 调用方拿着引用,GC 不能回收
$local → 闭包 use 捕获 → 闭包活着一天,$local 就活着一天
$nothing → 函数内用完就扔 → 函数返回时立即可回收,最理想
一句话终极结论
变量逃逸 = 函数里的变量没随函数结束一起销毁,反而被“带出去”继续活着;
逃逸的变量会让GC多操心、更占内存,不逃逸的变量用完就没,最省心。
用php-parser能直接找出两种最常见的逃逸:return带出去、被闭包抓走。
下面全程大白话,把概念、代码、原理拆到完全易懂。
一、先搞懂核心:啥是变量逃逸?
就按变量死没死、啥时候死来分:
1. 不逃逸(最理想)
变量在函数里创建、用完,函数一结束,变量直接销毁,GC立马回收内存,一点不占地方。
2. 逃逸(费内存)
变量在函数里创建,但没跟着函数一起死,被带出函数继续活着,
GC得一直盯着它,内存占用更久,压力更大。
二、代码里的两种逃逸(一眼看懂)
示例代码里造了两个逃逸变量,一个纯本地变量:
php
function process() {
$local = new stdClass(); // 被闭包抓走 → 逃逸
$escaped = new stdClass(); // 被return带出去 → 逃逸
$nothing = new stdClass(); // 函数内用完就扔 → 不逃逸
$fn = function() use ($local) { return $local; };
return $escaped;
}
1. $escaped :被 return 带出函数,外面的代码能拿到它 → 逃逸
2. $local :被闭包 use 抓走,闭包可能比函数活更久 → 逃逸
3. $nothing :函数里用完就没了,函数结束直接销毁 → 不逃逸
三、代码逐行大白话解释
1. 装依赖
bash
composer require nikic/php-parser
装一个能读懂PHP代码的工具,把代码拆成树形结构分析。
2. 准备要分析的代码
php
$code = <<<'PHP'
<?php
function process() {
$local = new stdClass();
$escaped = new stdClass();
$fn = function() use ($local) { return $local; };
return $escaped;
}
PHP;
把要查逃逸的PHP代码,当成一段字符串传进去。
3. 把代码转成AST(语法树)
php
$ast = (new ParserFactory)->createForNewestSupportedVersion()->parse($code);
把纯文本代码,拆成电脑能读懂的树形结构,每个语法(return、闭包、变量)都是一个节点。
4. 节点查找工具
php
$finder = new NodeFinder;
不用自己写递归遍历树,用这个工具直接找想要的语法节点。
5. 找「return带出的逃逸变量」
php
foreach ($finder->findInstanceOf($ast, Node\Stmt\Return_::class) as $ret) {
if ($ret->expr instanceof Node\Expr\Variable) {
echo "return 逃逸: \${$ret->expr->name}\n";
}
}
- 找所有 return 语句
- 检查return的是不是变量
- 是变量就说明它被带出函数 → 标记逃逸
- 这里找到: $escaped
6. 找「闭包捕获的逃逸变量」
php
foreach ($finder->findInstanceOf($ast, Node\Expr\ClosureUse::class) as $use) {
echo "闭包捕获逃逸: \${$use->var->name}\n";
}
- 找所有闭包 use(...) 里的变量
- 被闭包抓走的变量,活的时间比函数长 → 标记逃逸
- 这里找到: $local
四、两种逃逸的大白话后果
1. return逃逸($escaped)
函数把变量扔给了外部,外部一直拿着引用,GC想回收都收不了。
2. 闭包逃逸($local)
闭包像个“小背包”把变量装走了,只要闭包还在,变量就死不了,内存一直占着。
3. 不逃逸($nothing)
函数跑完,变量直接“原地去世”,内存立马释放,对GC最友好。
五、极简总结
1. 逃逸=变量没随函数销毁,被带出去活更久
2. 两种最常见逃逸: return 带出、闭包 use 捕获
3. 逃逸加重GC和内存压力,不逃逸最省资源
4. 用php-parser能自动扫描出所有逃逸的变量
更多推荐

所有评论(0)