PHP的垃圾回收機制詳解
最近由於使用php編寫了一個指令碼,模擬實現了一個守護程序,因此需要深入理解php中的垃圾回收機制。本文參考了PHP手冊。
在理解PHP垃圾回收機制(GC)之前,先了解一下變數的儲存。
php中變數存在於一個zval的變數容器中。結構如下:
型別 |
值 |
is_ref |
refcount |
zval中,除了儲存變數的型別和值之外,還有is_ref欄位和refcount欄位。
- is_ref:是個bool值,用來區分變數是否屬於引用集合。什麼意思呢,你可以這麼認為:表示變數是否有一個以上的別名。
- refcount:計數器,表示指向這個zval變數容器的變數個數。
兩者之間有這麼一個預設關係:當refcount值為1時,is_ref的值為false。因為refcount為1,此變數不可能有多個別名,也就不存在引用了。
安裝xdebug拓展之後,可以利用xdebug_debug_zval打印出zval容器詳情。
這裡有一點需要注意,將一個變數 = 賦值給另一個變數時,不會立即為新變數分配記憶體空間,而是在原變數的zval中給refcount加1。 只有當原變數或者發生改變時,才會為新變數分配記憶體空間,同時原變數的refcount減 1 。當然,如果unset原變數,新變數直接就使用原變數的zval而不是重新分配。
&引用賦值時,原變數的is_ref 變為1,refcount 加1. 如果給一個變數&賦值,之前 = 賦值的變數會分配空間。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php
$a
= 1;
xdebug_debug_zval(
'a'
);
echo
PHP_EOL;
$b
=
$a
;
xdebug_debug_zval(
'a'
);
echo
PHP_EOL;
$c
= &
$a
;
xdebug_debug_zval(
'a'
);
echo
PHP_EOL;
xdebug_debug_zval(
'b'
);
echo
PHP_EOL;
?>
|
執行結果如下:
a:(refcount=1, is_ref=0),int 1
a:(refcount=2, is_ref=0),int 1
a:(refcount=2, is_ref=1),int 1
b:(refcount=1, is_ref=0),int 1
上面描述的zval儲存的是標量,那複合型別的陣列是如何儲存的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php
$a
=
array
(
'meaning'
=>
'life'
,
'number'
=> 42 );
xdebug_debug_zval(
'a'
);
echo
PHP_EOL;
class
Test{
public
$a
= 1;
public
$b
= 2;
function
handle(){
echo
'hehe'
;
}
}
$test
=
new
Test();
xdebug_debug_zval(
'test'
);
?>
|
執行結果如下:
a:(refcount=1, is_ref=0),
array 'meaning' => (refcount=1, is_ref=0),
string
'life' (length=4)
'number' => (refcount=1, is_ref=0),
int
42
test:(refcount=1, is_ref=0),
object(Test)[1] public 'a' => (refcount=2, is_ref=0),
int
1
public 'b' => (refcount=2, is_ref=0),
int
2
可以看出,陣列用了比陣列長度多1個zval儲存。物件類似。下面給出了陣列的儲存形象表示
可以看到:陣列分配了三個zval容器:a meaning number
現在看看所謂的環狀引用是如何生成的
1 2 3 4 5 |
<?php
$a
=
array
(
'one'
);
$a
[] =&
$a
;
xdebug_debug_zval(
'a'
);
?>
|
執行結果:
a:(refcount=2, is_ref=1),
array 0 => (refcount=1, is_ref=0),
string
'one' (length=3)
1 => (refcount=2, is_ref=1), &array
a 和 1 的zval容器 是一樣的。如下:
這樣就形成了環狀引用。
在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變數空間是否能夠被釋放的時候是依據這個變數的zval的refcount的值,如果refcount為0,那麼變數的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。
現在unset ($a),那麼array的refcount減1變為1.現在無任何變數指向這個zval,而且這個zval的計數器為1,不會回收。
儘管不再有某個作用域中的任何符號指向這個結構(就是變數容器),由於陣列元素“1”仍然指向陣列本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,使用者沒有辦法清除這個結構,結果就會導致記憶體洩漏。慶幸的是,php將在請求結束時清除這個資料結構,但是在php清除之前,將耗費不少空間的記憶體。如果你要實現分析演算法,或者要做其他像一個子元素指向它的父元素這樣的事情,這種情況就會經常發生。當然,同樣的情況也會發生在物件上,實際上物件更有可能出現這種情況,因為物件總是隱式的被引用。
如果上面的情況發生僅僅一兩次倒沒什麼,但是如果出現幾千次,甚至幾十萬次的記憶體洩漏,這顯然是個大問題。在長時間執行的指令碼,比如請求基本上不會結束的守護程序時,就會出現問題,記憶體空間會不斷耗費,導致記憶體不足而崩潰。
PHP5.3中,採用了專門的演算法(比較複雜)。,來處理環狀引用導致記憶體洩露的問題。
當一個zval可能為垃圾時,回收演算法會把這個zval放入一個記憶體緩衝區。當緩衝區達到最大臨界值時(最大值可以設定),回收演算法會迴圈遍歷所有緩衝區中的zval,判斷其是否為垃圾,並進行釋放處理。或者我們在指令碼中使用gc_collect_cycles,強制回收緩衝區中的垃圾。
在php5.3的GC中,針對的垃圾做了如下說明:
1:如果一個zval的refcount增加,那麼此zval還在使用,肯定不是垃圾,不會進入緩衝區
2:如果一個zval的refcount減少到0, 那麼zval會被立即釋放掉,不屬於GC要處理的垃圾物件,不會進入緩衝區。
3:如果一個zval的refcount減少之後大於0,那麼此zval還不能被釋放,此zval可能成為一個垃圾,將其放入緩衝區。PHP5.3中的GC針對的就是這種zval進行的處理。
開啟/關閉垃圾回收機制可以通過修改php配置實現,也可以在程式中使用gc_enable() 和 gc_disable()開啟和關閉。
開啟垃圾回收機制後,針對記憶體洩露的情況,可以節省大量的記憶體空間,但是由於垃圾回收演算法執行耗費時間,開啟垃圾回收演算法會增加指令碼的執行時間。
下面是php手冊中給的一個指令碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php
class
Foo
{
public
$var
=
'3.1415962654'
;
}
$baseMemory
= memory_get_usage();
for
(
$i
= 0;
$i
<= 100000;
$i
++ )
{
$a
=
new
Foo;
$a
->self =
$a
;
if
(
$i
% 500 === 0 )
{
echo
sprintf(
'%8d: '
,
$i
), memory_get_usage() -
$baseMemory
,
"\n"
;
}
}
?>
|
針對這個指令碼,給出了其在php5.2和5.3中記憶體的佔用情況,如下圖:
針對下面這個指令碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php
class
Foo
{
public
$var
=
'3.1415962654'
;
}
for
(
$i
= 0;
$i
<= 1000000;
$i
++ )
{
$a
=
new
Foo;
$a
->self =
$a
;
}
echo
memory_get_peak_usage(),
"\n"
;
?>
|
開啟垃圾回收機制,相對於不開啟的時候,指令碼執行時間增加了7%
通常,PHP中的垃圾回收機制,僅僅在迴圈回收演算法確實執行時會有時間消耗上的增加。但是在平常的(更小的)指令碼中應根本就沒有效能影響。