php的垃圾回收機制

在平時php-fpm的時候,可能很少人注意php的變量回收,但是到swoole常駐內存開發後,就不得不重視這個了,因爲在常駐內存下,如果不瞭解變量回收機制,可能就會出現內存泄露的問題,本文將一步步帶你瞭解php的垃圾回收機制,讓你寫出的代碼不再內存泄漏

 

寫時複製

首先,php的變量複製用的是寫時複製方式,舉個例子. 

 

$a='仙士可'.time();

$b=$a;

$c=$a;

//這個時候內存佔用相同,$b,$c都將指向$a的內存,無需額外佔用

 

$b='仙士可1號';

//這個時候$b的數據已經改變了,無法再引用$a的內存,所以需要額外給$b開拓內存空間

 

$a='仙士可2號';

//$a的數據發生了變化,同樣的,$c也無法引用$a了,需要給$a額外開拓內存空間

詳細寫時複製可查看:php寫時複製

 

引用計數

既然變量會引用內存,那麼刪除變量的時候,就會出現一個問題了:

 

$a='仙士可';

$b=$a;

$c=$a;

//這個時候內存佔用相同,$b,$c都將指向$a的內存,無需額外佔用

 

$b='仙士可1號';

//這個時候$b的數據已經改變了,無法再引用$a的內存,所以需要額外給$b開拓內存空間

 

unset($c);

//這個時候,刪除$c,由於$c的數據是引用$a的數據,那麼直接刪除$a?

很明顯,當$c引用$a的時候,刪除$c,不能把$a的數據直接給刪除,那麼該怎麼做呢?

這個時候,php底層就使用到了引用計數這個概念

引用計數,給變量引用的次數進行計算,當計數不等於0時,說明這個變量已經被引用,不能直接被回收,否則可以直接回收,例如:

 

$a '仙士可'.time();

$b $a;

$c $a;

 

xdebug_debug_zval('a');

xdebug_debug_zval('b');

xdebug_debug_zval('c');

 

$b='仙士可2號';

xdebug_debug_zval('a');

xdebug_debug_zval('b');

 

echo "腳本結束\n";

 

將輸出:

 

a: (refcount=3, is_ref=0)='仙士可1578154814'

b: (refcount=3, is_ref=0)='仙士可1578154814'

c: (refcount=3, is_ref=0)='仙士可1578154814'

a: (refcount=2, is_ref=0)='仙士可1578154814'

b: (refcount=1, is_ref=0)='仙士可2號'

腳本結束

 

注意,xdebug_debug_zval函數是xdebug擴展的,使用前必須安裝xdebug擴展

 

引用計數特殊情況

當變量值爲整型,浮點型時,在賦值變量時,php7底層將會直接把值存儲(php7的結構體將會直接存儲簡單數據類型),refcount將爲0

 

$a = 1111;

$b $a;

$c = 22.222;

$d $c;

 

xdebug_debug_zval('a');

xdebug_debug_zval('b');

xdebug_debug_zval('c');

xdebug_debug_zval('d');

echo "腳本結束\n";

輸出:

 

a: (refcount=0, is_ref=0)=1111

b: (refcount=0, is_ref=0)=1111

c: (refcount=0, is_ref=0)=22.222

d: (refcount=0, is_ref=0)=22.222

腳本結束

 

當變量值爲interned string字符串型(變量名,函數名,靜態字符串,類名等)時,變量值存儲在靜態區,內存回收被系統全局接管,引用計數將一直爲1(php7.3)

 

$str = '仙士可';    // 靜態字符串

$str = '仙士可' . time();//普通字符串

 

$a 'aa';

$b $a;

$c $b;

 

$d 'aa'.time();

$e $d;

$f $d;

 

xdebug_debug_zval('a');

xdebug_debug_zval('d');

echo "腳本結束\n";

輸出:

 

a: (refcount=1, is_ref=0)='aa'

d: (refcount=3, is_ref=0)='aa1578156506'

腳本結束

 

當變量值爲以上幾種時,複製變量將會直接拷貝變量值,所以將不存在多次引用的情況

 

引用時引用計數變化

如下代碼:

 

$a 'aa';

$b = &$a;

$c $b;

 

xdebug_debug_zval('a');

xdebug_debug_zval('b');

xdebug_debug_zval('c');

echo "腳本結束\n";

將輸出:

 

a: (refcount=2, is_ref=1)='aa'

b: (refcount=2, is_ref=1)='aa'

c: (refcount=1, is_ref=0)='aa'

腳本結束

當引用時,被引用變量的value以及類型將會更改爲引用類型,並將引用值指向原來的值內存地址中.

之後引用變量的類型也會更改爲引用類型,並將值指向原來的值內存地址,這個時候,值內存地址被引用了2次,所以refcount=2.

而$c並非是引用變量,所以將值複製給了$c,$c引用還是爲1

 

詳細引用計數知識,底層原理可查看:https://www.cnblogs.com/sohuhome/p/9800977.html

 

php生命週期

php將每個運行域作爲一次生命週期,每次執行完一個域,將回收域內所有相關變量:

 

<?php

/**

 * Created by PhpStorm.

 * User: Tioncico

 * Date: 2020/1/6 0006

 * Time: 14:22

 */

 

echo "php文件的全局開始\n";

 

class A{

    protected $a;

    function __construct($a)

    {

        $this->a = $a;

        echo "類A{$this->a}生命週期開始\n";

    }

    function test(){

        echo "類test方法域開始\n";

        echo "類test方法域結束\n";

    }

//通過類析構函數的特性,當類初始化或回收時,會調用相應的方法

    function __destruct()

    {

        echo "類A{$this->a}生命週期結束\n";

        // TODO: Implement __destruct() method.

    }

}

 

function a1(){

    echo "a1函數域開始\n";

    $a new A(1);

    echo "a1函數域結束\n";

    //函數結束,將回收所有在函數a1的變量$a

}

a1();

 

$a new A(2);

 

echo "php文件的全局結束\n";

//全局結束後,會回收全局的變量$a

 

可看出,每個方法/函數都作爲一個作用域,當運行完該作用域時,將會回收這裏面的所有變量.

 

再看看這個例子:

 

echo "php文件的全局開始\n";

 

class A

{

    protected $a;

 

    function __construct($a)

    {

        $this->a = $a;

        echo "類{$this->a}生命週期開始\n";

    }

 

    function test()

    {

        echo "類test方法域開始\n";

        echo "類test方法域結束\n";

    }

 

//通過類析構函數的特性,當類初始化或回收時,會調用相應的方法

    function __destruct()

    {

        echo "類{$this->a}生命週期結束\n";

        // TODO: Implement __destruct() method.

    }

}

 

$arr = [];

$i = 0;

while (1) {

    $arr[] = new A('arr_' $i);

    $obj new A('obj_' $i);

    $i++;

    echo "數組大小:"count($arr).'\n';

    sleep(1);

//$arr 會隨着循環,慢慢的變大,直到內存溢出

 

}

 

echo "php文件的全局結束\n";

//全局結束後,會回收全局的變量$a

全局變量只有在腳本結束後纔會回收,而在這份代碼中,腳本永遠不會被結束,也就說明變量永遠不會回收,$arr還在不斷的增加變量,直到內存溢出.

 

內存泄漏

請看代碼:

 

function a(){

    class A {

        public $ref;

        public $name;

 

        public function __construct($name) {

            $this->name = $name;

            echo($this->name.'->__construct();'.PHP_EOL);

        }

 

        public function __destruct() {

            echo($this->name.'->__destruct();'.PHP_EOL);

        }

    }

 

    $a1 new A('$a1');

    $a2 new A('$a2');

    $a3 new A('$3');

 

    $a1->ref = $a2;

    $a2->ref = $a1;

 

    unset($a1);

    unset($a2);

 

    echo('exit(1);'.PHP_EOL);

}

a();

echo('exit(2);'.PHP_EOL);

當$a1和$a2的屬性互相引用時,unset($a1,$a2) 只能刪除變量的引用,卻沒有真正的刪除類的變量,這是爲什麼呢?

首先,類的實例化變量分爲2個步驟,1:開闢類存儲空間,用於存儲類數據,2:實例化一個變量,類型爲class,值指向類存儲空間.

當給變量賦值成功後,類的引用計數爲1,同時,a1->ref指向了a2,導致a2類引用計數增加1,同時a1類被a2->ref引用,a1引用計數增加1

當unset時,只會刪除類的變量引用,也就是-1,但是該類其實還存在了一次引用(類的互相引用),

這將造成這2個類內存永遠無法釋放,直到被gc機制循環查找回收,或腳本終止回收(域結束無法回收).

 

手動回收機制

在上面,我們知道了腳本回收,域結束回收2種php回收方式,那麼可以手動回收嗎?答案是可以的.

手動回收有以下幾種方式:

unset,賦值爲null,變量賦值覆蓋,gc_collect_cycles函數回收

 

 

unset

unset爲最常用的一種回收方式,例如:

 

class A

{

    public $ref;

    public $name;

 

    public function __construct($name)

    {

        $this->name = $name;

        echo($this->name . '->__construct();' . PHP_EOL);

    }

 

    public function __destruct()

    {

        echo($this->name . '->__destruct();' . PHP_EOL);

    }

}

 

$a new A('$a');

$b new A('$b');

unset($a);

//a將會先回收

echo('exit(1);' . PHP_EOL);

//b需要腳本結束纔會回收

輸出:

 

$a->__construct();

$b->__construct();

$a->__destruct();

exit(1);

$b->__destruct();

 

unset的回收原理其實就是引用計數-1,當引用計數-1之後爲0時,將會直接回收該變量,否則不做操作(這就是上面內存泄漏的原因,引用計數-1並沒有等於0)

 

=null回收

 

class A

{

    public $ref;

    public $name;

 

    public function __construct($name)

    {

        $this->name = $name;

        echo($this->name . '->__construct();' . PHP_EOL);

    }

 

    public function __destruct()

    {

        echo($this->name . '->__destruct();' . PHP_EOL);

    }

}

 

$a new A('$a');

$b new A('$b');

$c new A('$c');

unset($a);

$c=null;

xdebug_debug_zval('a');

xdebug_debug_zval('b');

xdebug_debug_zval('c');

 

echo('exit(1);' . PHP_EOL);

=null和unset($a),作用其實都爲一致,null將變量值賦值爲null,原先的變量值引用計數-1,而unset是將變量名從php底層變量表中清理,並將變量值引用計數-1,唯一的區別在於,=null,變量名還存在,而unset之後,該變量就沒了:

 

$a->__construct();

$b->__construct();

$c->__construct();

$a->__destruct();

$c->__destruct();

a: no such symbol //$a已經不在符號表

b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' }

c: (refcount=0, is_ref=0)=NULL  //c還存在,只是值爲null

exit(1);

$b->__destruct();

 

變量覆蓋回收

通過給變量賦值其他值(例如null)進行回收:

 

class A

{

    public $ref;

    public $name;

 

    public function __construct($name)

    {

        $this->name = $name;

        echo($this->name . '->__construct();' . PHP_EOL);

    }

 

    public function __destruct()

    {

        echo($this->name . '->__destruct();' . PHP_EOL);

    }

}

 

$a new A('$a');

$b new A('$b');

$c new A('$c');

$a=null;

$c'練習時長兩年半的個人練習生';

xdebug_debug_zval('a');

xdebug_debug_zval('b');

xdebug_debug_zval('c');

 

echo('exit(1);' . PHP_EOL);

將輸出:

 

$a->__construct();

$b->__construct();

$c->__construct();

$a->__destruct();

$c->__destruct();

a: (refcount=0, is_ref=0)=NULL

b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' }

c: (refcount=1, is_ref=0)='練習時長兩年半的個人練習生'

exit(1);

$b->__destruct();

可以看出,c由於覆蓋賦值,將原先A類實例的引用計數-1,導致了$c的回收,但是從程序的內存佔用來說,覆蓋變量並不是意義上的內存回收,只是將變量的內存修改爲了其他值.內存不會直接清空.

 

gc_collect_cycles

回到之前的內存泄漏章節,當寫程序不小心造成了內存泄漏,內存越來越大,可是php默認只能腳本結束後回收,那該怎麼辦呢?我們可以使用gc_collect_cycles 函數,進行手動回收

 

function a(){

    class A {

        public $ref;

        public $name;

 

        public function __construct($name) {

            $this->name = $name;

            echo($this->name.'->__construct();'.PHP_EOL);

        }

 

        public function __destruct() {

            echo($this->name.'->__destruct();'.PHP_EOL);

        }

    }

 

    $a1 new A('$a1');

    $a2 new A('$a2');

 

    $a1->ref = $a2;

    $a2->ref = $a1;

 

    $b new A('$b');

    $b->ref = $a1;

 

    echo('$a1 = $a2 = $b = NULL;'.PHP_EOL);

    $a1 $a2 $b = NULL;

    echo('gc_collect_cycles();'.PHP_EOL);

    echo('// removed cycles: '.gc_collect_cycles().PHP_EOL);

    //這個時候,a1,a2已經被gc_collect_cycles手動回收了

    echo('exit(1);'.PHP_EOL);

 

}

a();

echo('exit(2);'.PHP_EOL);

輸出:

 

$a1->__construct();

$a2->__construct();

$b->__construct();

$a1 = $a2 = $b = NULL;

$b->__destruct();

gc_collect_cycles();

$a1->__destruct();

$a2->__destruct();

// removed cycles: 4

exit(1);

exit(2);

注意,gc_colect_cycles 函數會從php的符號表,遍歷所有變量,去實現引用計數的計算並清理內存,將消耗大量的cpu資源,不建議頻繁使用

另外,除去這些方法,php內存到達一定臨界值時,會自動調用內存清理(我猜的),每次調用都會消耗大量的資源,可通過gc_disable 函數,去關閉php的自動gc

發佈了198 篇原創文章 · 獲贊 26 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章