先说概念:

  逃逸 = 变量活过了它被创建的那个函数
  不逃逸 = 变量在函数结束时就死了

  逃逸的变量 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";
  被闭包捕获的变量也算逃逸,因为闭包可能活得比原函数更久,变量不能提前释放。

  ---
  两种逃逸对比:

  $escapedreturn 带出函数 → 调用方拿着引用,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能自动扫描出所有逃逸的变量
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐