利用assembly.xml打包maven專案報錯
在平時php-fpm的時候,可能很少人注意php的變量回收,但是到swoole常駐記憶體開發後,就不得不重視這個了,因為在常駐記憶體下,如果不瞭解變量回收機制,可能就會出現記憶體洩露的問題,本文將一步步帶你瞭解php的垃圾回收機制,讓你寫出的程式碼不再記憶體洩漏
寫時複製
首先,php的變數複製用的是寫時複製方式,舉個例子.
1 2 3 4 5 6 7 8 9 10 |
$a = '仙士可' .time();
$b = $a ;
$c = $a ;
//這個時候記憶體佔用相同,$b,$c都將指向$a的記憶體,無需額外佔用
$b = '仙士可1號' ;
//這個時候$b的資料已經改變了,無法再引用$a的記憶體,所以需要額外給$b開拓記憶體空間 $a = '仙士可2號' ;
//$a的資料發生了變化,同樣的,$c也無法引用$a了,需要給$a額外開拓記憶體空間
|
詳細寫時複製可檢視:php寫時複製
引用計數
既然變數會引用記憶體,那麼刪除變數的時候,就會出現一個問題了:
1 2 3 4 5 6 7 8 9 10 |
$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時,說明這個變數已經被引用,不能直接被回收,否則可以直接回收,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$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" ;
|
將輸出:
1 2 3 4 5 6 |
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
1 2 3 4 5 6 7 8 9 10 |
$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" ;
|
輸出:
1 2 3 4 5 |
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();//普通字串
1 2 3 4 5 6 7 8 9 10 11 |
$a = 'aa' ;
$b = $a ;
$c = $b ;
$d = 'aa' .time();
$e = $d ;
$f = $d ;
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'd' );
echo "指令碼結束\n" ;
|
輸出:
1 2 3 |
a:(refcount=1,is_ref=0)= 'aa'
d:(refcount=3,is_ref=0)= 'aa1578156506'
指令碼結束
|
當變數值為以上幾種時,複製變數將會直接拷貝變數值,所以將不存在多次引用的情況
引用時引用計數變化
如下程式碼:
1 2 3 4 5 6 7 8 |
$a = 'aa' ;
$b =& $a ;
$c = $b ;
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
xdebug_debug_zval( 'c' );
echo "指令碼結束\n" ;
|
將輸出:
1 2 3 4 |
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將每個執行域作為一次生命週期,每次執行完一個域,將回收域內所有相關變數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?php
/**
*CreatedbyPhpStorm.
*User:Tioncico
*Date:2020/1/60006
*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
|
可看出,每個方法/函式都作為一個作用域,當執行完該作用域時,將會回收這裡面的所有變數.
再看看這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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還在不斷的增加變數,直到記憶體溢位.
記憶體洩漏
請看程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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為最常用的一種回收方式,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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需要指令碼結束才會回收
|
輸出:
1 2 3 4 5 |
$a->__construct();
$b->__construct();
$a->__destruct();
exit (1);
$b->__destruct();
|
unset的回收原理其實就是引用計數-1,當引用計數-1之後為0時,將會直接回收該變數,否則不做操作(這就是上面記憶體洩漏的原因,引用計數-1並沒有等於0)
=null回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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之後,該變數就沒了:
1 2 3 4 5 6 7 8 9 10 |
$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a:nosuchsymbol // $a已經不在符號表
b:(refcount=1,is_ref=0)=classA{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)進行回收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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);
|
將輸出:
1 2 3 4 5 6 7 8 9 10 |
$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a:(refcount=0,is_ref=0)=NULL
b:(refcount=1,is_ref=0)=classA{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函式,進行手動回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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 ( '//removedcycles:' .gc_collect_cycles().PHP_EOL);
//這個時候,a1,a2已經被gc_collect_cycles手動回收了
echo ( 'exit(1);' .PHP_EOL);
}
a();
echo ( 'exit(2);' .PHP_EOL);
|
輸出:
1 2 3 4 5 6 7 8 9 10 11 |
$a1->__construct();
$a2->__construct();
$b->__construct();
$a1=$a2=$b=NULL;
$b->__destruct();
gc_collect_cycles();
$a1->__destruct();
$a2->__destruct();
// removedcycles:4
exit (1);
exit (2);
|
注意,gc_colect_cycles 函式會從php的符號表,遍歷所有變數,去實現引用計數的計算並清理記憶體,將消耗大量的cpu資源,不建議頻繁使用
另外,除去這些方法,php記憶體到達一定臨界值時,會自動呼叫記憶體清理(我猜的),每次呼叫都會消耗大量的資源,可通過gc_disable 函式,去關閉php的自動gc
參考:http://www.php20.cn/article/230