先说概念:
逃逸 = 变量活过了它被创建的那个函数
不逃逸 = 变量在函数结束时就死了
逃逸的变量 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 → 函数内用完就扔 → 函数返回时立即可回收,最理想






