wuliao 2017-07-19 15:43:46 9912次浏览 6条评论 8 4 0

1 发现问题
同事跑过来一个问题(下面这段代码内存会一直增长,为什么?

public static function actionTest() {
        $total = 10;
        var_dump('开始内存'.memory_get_usage());
        while($total){
            $ret=User::findOne(['id'=>910002]);
            var_dump('end内存'.memory_get_usage());
            unset($ret);
            $total--;
        }
    }

上面代码的内存一直在增长, 按照原本想法来看, 变量被释放了,内存就算增长也不会一直增长。因为每循环一次内存都会被释放。

2 分析问题
上面这段代码涉及到了数据库的操作,而我们知道,数据库的很多地方都能引起内存泄漏。 所以先屏蔽数据库相关操作,
我手写了一个原生的数据库查询操作, 发现内存正常,没有问题。

$dsn = "mysql:dbname=test;host=localhost";
$db_user = 'root';
$db_pass = 'admin';
//查询
$sql = "select * from buyer";
$res = $pdo->query($sql);
foreach($res as $row) {
    echo $row['username'].'<br/>';
}

这时候答案呼之欲出--- 是yii2框架搞了鬼

3 定位问题
既然知道了是yii2 框架的问题那就可以进一步缩小问题。

public static function actionTest() {
        $total = 10;
        var_dump('开始内存'.memory_get_usage());
        while($total){
            $ret= new User();
            var_dump('end内存'.memory_get_usage());
            unset($ret);
            $total--;
        }
    } 

内存还是一直增长。 这时候我测试了一个其他的yii2类 发觉内存不增长了。
这就可以联想到是在new 对象的时候yii2内部自己执行了什么操作,然后导致内存泄漏。
什么方法是new 的时候就执行的呢。。。
对的 构造方法 __construct 。
然后 我一步一步的从model 查到object 发觉都没有能引起泄漏的地方。

这个时候我们不妨换个思路, 既然是yii2框架下出现的泄漏, 那肯定就是yii2独有的功能,
那什么功能是yii2独有的,又是在new 对象的时候就会执行的呢?

3.1 行为(Behavior)
发觉我的模型类里面果然有用了行为

public function behaviors()
    {
        return [
            TimestampBehavior::class,
        ];
    }

最普通不过的代码。
我们知道 行为最后调用的地方是
yii\base\Component->attachBehaviors
最后定位到

private function attachBehaviorInternal($name, $behavior)
    {
        if (!($behavior instanceof Behavior)) {
            $behavior = Yii::createObject($behavior);
        }
        if (is_int($name)) {
            $behavior->attach($this);
            $this->_behaviors[] = $behavior;
        } else {
            if (isset($this->_behaviors[$name])) {
                $this->_behaviors[$name]->detach();
            }
            $behavior->attach($this);
            $this->_behaviors[$name] = $behavior;
        }
 
        return $behavior;
    }

我们观察这段代码,发觉他吧自己传进去了$behavior->attach($this);
最后调用的是 yii\base\Behavior->attach

public function attach($owner)
    {
        $this->owner = $owner;
        foreach ($this->events() as $event => $handler) {
            $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
        }
    }

这个时候答案已经呼之欲出, yii2为了实现行为这一功能, 把自身this传进去,以便能注册事件,触发事件。解除事件。
这就导致了一个循环引用的问题。 所以导致对象refcount一直不为0 一直回收不了。

关于php垃圾回收机制,yii2的行为相关知识等还是自己查资料吧

觉得很赞
亿速云
  • 评论于 2017-08-03 18:07 举报

    厉害 就服你这种类型的高手

  • 评论于 2017-09-18 23:49 举报

    赶紧提交bug啊

  • 评论于 2018-01-30 19:50 举报

    我今天做导出的时候也遇到了同样的问题,使用原生createCommand的方法执行sql,每次循环内存仍然没法释放

  • 评论于 2018-02-20 03:19 举报

    不是yii的这个问题,而是你开启了debug模式,原生的sql(非yii带的原生)是不会记录在debug的日志中,所以内存不会增长,如果你关闭debug,你再测试下你的代码,会发现内存没有怎么增长

  • 评论于 2019-05-28 15:59 举报

    设置 enableProfiling enableLogging为false即可解决

    觉得很赞
  • 评论于 2020-01-08 12:08 举报

    每一次循环之后,执行:
    gc_collect_cycles();
    php7下测试有效,可以释放内存,很好用。
    内存释放之后,不仅不会内存溢出了,脚本执行速度也快了很多

    1 条回复
    评论于 2020-01-08 12:33 回复

    php7.2下测试有效。7.0还是有问题

您需要登录后才可以评论。登录 | 立即注册