溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

php中的垃圾回收機(jī)制

發(fā)布時間:2020-06-19 13:00:35 來源:億速云 閱讀:139 作者:鴿子 欄目:編程語言

在平時php-fpm的時候,可能很少人注意php的變量回收,但是到swoole常駐內(nèi)存開發(fā)后,就不得不重視這個了,因?yàn)樵诔qv內(nèi)存下,如果不了解變量回收機(jī)制,可能就會出現(xiàn)內(nèi)存泄露的問題,本文將一步步帶你了解php的垃圾回收機(jī)制,讓你寫出的代碼不再內(nèi)存泄漏

寫時復(fù)制

首先,php的變量復(fù)制用的是寫時復(fù)制方式,舉個例子.

$a='仙士可'.time();
$b=$a;
$c=$a;
//這個時候內(nèi)存占用相同,$b,$c都將指向$a的內(nèi)存,無需額外占用
 
$b='仙士可1號';
//這個時候$b的數(shù)據(jù)已經(jīng)改變了,無法再引用$a的內(nèi)存,所以需要額外給$b開拓內(nèi)存空間
 
$a='仙士可2號';
//$a的數(shù)據(jù)發(fā)生了變化,同樣的,$c也無法引用$a了,需要給$a額外開拓內(nèi)存空間

詳細(xì)寫時復(fù)制可查看:php寫時復(fù)制

引用計(jì)數(shù)

既然變量會引用內(nèi)存,那么刪除變量的時候,就會出現(xiàn)一個問題了:

$a='仙士可';
$b=$a;
$c=$a;
//這個時候內(nèi)存占用相同,$b,$c都將指向$a的內(nèi)存,無需額外占用
 
$b='仙士可1號';
//這個時候$b的數(shù)據(jù)已經(jīng)改變了,無法再引用$a的內(nèi)存,所以需要額外給$b開拓內(nèi)存空間
 
unset($c);
//這個時候,刪除$c,由于$c的數(shù)據(jù)是引用$a的數(shù)據(jù),那么直接刪除$a?

很明顯,當(dāng)$c引用$a的時候,刪除$c,不能把$a的數(shù)據(jù)直接給刪除,那么該怎么做呢?

這個時候,php底層就使用到了引用計(jì)數(shù)這個概念

引用計(jì)數(shù),給變量引用的次數(shù)進(jìn)行計(jì)算,當(dāng)計(jì)數(shù)不等于0時,說明這個變量已經(jīng)被引用,不能直接被回收,否則可以直接回收,例如:

$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 "腳本結(jié)束\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號'
腳本結(jié)束

注意,xdebug_debug_zval函數(shù)是xdebug擴(kuò)展的,使用前必須安裝xdebug擴(kuò)展

引用計(jì)數(shù)特殊情況

當(dāng)變量值為整型,浮點(diǎn)型時,在賦值變量時,php7底層將會直接把值存儲(php7的結(jié)構(gòu)體將會直接存儲簡單數(shù)據(jù)類型),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 "腳本結(jié)束\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
腳本結(jié)束

當(dāng)變量值為interned string字符串型(變量名,函數(shù)名,靜態(tài)字符串,類名等)時,變量值存儲在靜態(tài)區(qū),內(nèi)存回收被系統(tǒng)全局接管,引用計(jì)數(shù)將一直為1(php7.3)

$str = '仙士可';    // 靜態(tài)字符串

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

$a = 'aa';
$b = $a;
$c = $b;
 
$d = 'aa'.time();
$e = $d;
$f = $d;
 
xdebug_debug_zval('a');
xdebug_debug_zval('d');
echo "腳本結(jié)束\n";

輸出:

a: (refcount=1, is_ref=0)='aa'
d: (refcount=3, is_ref=0)='aa1578156506'
腳本結(jié)束

當(dāng)變量值為以上幾種時,復(fù)制變量將會直接拷貝變量值,所以將不存在多次引用的情況

引用時引用計(jì)數(shù)變化

如下代碼:

$a = 'aa';
$b = &$a;
$c = $b;
 
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
echo "腳本結(jié)束\n";

將輸出:

a: (refcount=2, is_ref=1)='aa'
b: (refcount=2, is_ref=1)='aa'
c: (refcount=1, is_ref=0)='aa'
腳本結(jié)束

當(dāng)引用時,被引用變量的value以及類型將會更改為引用類型,并將引用值指向原來的值內(nèi)存地址中.

之后引用變量的類型也會更改為引用類型,并將值指向原來的值內(nèi)存地址,這個時候,值內(nèi)存地址被引用了2次,所以refcount=2.

而$c并非是引用變量,所以將值復(fù)制給了$c,$c引用還是為1

詳細(xì)引用計(jì)數(shù)知識,底層原理可查看:https://www.cnblogs.com/sohuhome/p/9800977.html

php生命周期

php將每個運(yùn)行域作為一次生命周期,每次執(zhí)行完一個域,將回收域內(nèi)所有相關(guān)變量:

<?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方法域結(jié)束\n";
    }
//通過類析構(gòu)函數(shù)的特性,當(dāng)類初始化或回收時,會調(diào)用相應(yīng)的方法
    function __destruct()
    {
        echo "類A{$this->a}生命周期結(jié)束\n";
        // TODO: Implement __destruct() method.
    }
}
 
function a1(){
    echo "a1函數(shù)域開始\n";
    $a = new A(1);
    echo "a1函數(shù)域結(jié)束\n";
    //函數(shù)結(jié)束,將回收所有在函數(shù)a1的變量$a
}
a1();
 
$a = new A(2);
 
echo "php文件的全局結(jié)束\n";
//全局結(jié)束后,會回收全局的變量$a

可看出,每個方法/函數(shù)都作為一個作用域,當(dāng)運(yùn)行完該作用域時,將會回收這里面的所有變量.

再看看這個例子:

echo "php文件的全局開始\n";
 
class A
{
    protected $a;
 
    function __construct($a)
    {
        $this->a = $a;
        echo "類{$this->a}生命周期開始\n";
    }
 
    function test()
    {
        echo "類test方法域開始\n";
        echo "類test方法域結(jié)束\n";
    }
 
//通過類析構(gòu)函數(shù)的特性,當(dāng)類初始化或回收時,會調(diào)用相應(yīng)的方法
    function __destruct()
    {
        echo "類{$this->a}生命周期結(jié)束\n";
        // TODO: Implement __destruct() method.
    }
}
 
$arr = [];
$i = 0;
while (1) {
    $arr[] = new A('arr_' . $i);
    $obj = new A('obj_' . $i);
    $i++;
    echo "數(shù)組大小:". count($arr).'\n';
    sleep(1);
//$arr 會隨著循環(huán),慢慢的變大,直到內(nèi)存溢出
 
}
 
echo "php文件的全局結(jié)束\n";
//全局結(jié)束后,會回收全局的變量$a

全局變量只有在腳本結(jié)束后才會回收,而在這份代碼中,腳本永遠(yuǎn)不會被結(jié)束,也就說明變量永遠(yuǎn)不會回收,$arr還在不斷的增加變量,直到內(nèi)存溢出.

內(nèi)存泄漏

請看代碼:

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);

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

首先,類的實(shí)例化變量分為2個步驟,1:開辟類存儲空間,用于存儲類數(shù)據(jù),2:實(shí)例化一個變量,類型為class,值指向類存儲空間.

當(dāng)給變量賦值成功后,類的引用計(jì)數(shù)為1,同時,a1->ref指向了a2,導(dǎo)致a2類引用計(jì)數(shù)增加1,同時a1類被a2->ref引用,a1引用計(jì)數(shù)增加1

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

這將造成這2個類內(nèi)存永遠(yuǎn)無法釋放,直到被gc機(jī)制循環(huán)查找回收,或腳本終止回收(域結(jié)束無法回收).

手動回收機(jī)制

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

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

unset,賦值為null,變量賦值覆蓋,gc_collect_cycles函數(shù)回收

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需要腳本結(jié)束才會回收

輸出:

$a->__construct();
$b->__construct();
$a->__destruct();
exit(1);
$b->__destruct();

unset的回收原理其實(shí)就是引用計(jì)數(shù)-1,當(dāng)引用計(jì)數(shù)-1之后為0時,將會直接回收該變量,否則不做操作(這就是上面內(nèi)存泄漏的原因,引用計(jì)數(shù)-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),作用其實(shí)都為一致,null將變量值賦值為null,原先的變量值引用計(jì)數(shù)-1,而unset是將變量名從php底層變量表中清理,并將變量值引用計(jì)數(shù)-1,唯一的區(qū)別在于,=null,變量名還存在,而unset之后,該變量就沒了:

$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a: no such symbol //$a已經(jīng)不在符號表
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)進(jìn)行回收:

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= '練習(xí)時長兩年半的個人練習(xí)生';
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)='練習(xí)時長兩年半的個人練習(xí)生'
exit(1);
$b->__destruct();

可以看出,c由于覆蓋賦值,將原先A類實(shí)例的引用計(jì)數(shù)-1,導(dǎo)致了$c的回收,但是從程序的內(nèi)存占用來說,覆蓋變量并不是意義上的內(nèi)存回收,只是將變量的內(nèi)存修改為了其他值.內(nèi)存不會直接清空.

gc_collect_cycles

回到之前的內(nèi)存泄漏章節(jié),當(dāng)寫程序不小心造成了內(nèi)存泄漏,內(nèi)存越來越大,可是php默認(rèn)只能腳本結(jié)束后回收,那該怎么辦呢?我們可以使用gc_collect_cycles 函數(shù),進(jìn)行手動回收

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已經(jīng)被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 函數(shù)會從php的符號表,遍歷所有變量,去實(shí)現(xiàn)引用計(jì)數(shù)的計(jì)算并清理內(nèi)存,將消耗大量的cpu資源,不建議頻繁使用

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

以上就是探討php的垃圾回收機(jī)制的詳細(xì)內(nèi)容,更多請關(guān)注億速云其它相關(guān)文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI