PHP 的垃圾回收機制
摘要:b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL。b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL。
在平時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
其他
以上就是全部內容了,如果發現文章有錯,希望指出,也可以加我好友互相討論
----------偉大的分割線-----------
PHP飯米粒(phpfamily) 由一羣靠譜的人建立,願爲PHPer帶來一些值得細細品味的精神食糧!
飯米粒只發原創或授權發表的文章,不轉載網上的文章
所發的文章,均可找到原作者進行溝通。
也希望各位多多打賞(算作稿費給文章作者),更希望大家多多投 稿 。
投稿請聯繫:
本文由 仙士可 授權 飯米粒 發佈,轉載請註明本來源信息和以下的二維碼(長按可識別二維碼關注)