前言:之前對PHP的GC只是了解了個大概,這次詳細了解下PHP的垃圾回收機制(GC)。
介於網上大部分都是PHP5.X的GC,雖然 php5 到 php7 GC部分做出的改動較小,但我覺得還是一起寫下來比較好
一、原理
php5和php7的垃圾回收機制都是利用引用計數
二、php5和php7不同點
1、PHP5標量數據類型會計數,PHP7標量數據類型不再計數,不需要單獨分配內存 2、PHP7的zval 需要的內存不再是單獨從堆上分配,不再自己存儲引用計數。 3、PHP7的復雜數據類型(比如數組和對象)的引用計數由其自身來存儲。
三、變量在zval的變量容器中結構
zval中,除了存儲變量的類型和值之外,還有is_ref字段和refcount字段 1、is_ref:是個bool值,用來區分變量是否屬於引用集合。 2、refcount:計數器,表示指向這個zval變量容器的變量個數。
四、PHP5.3標量在zval容器例子
注意:php5.3中將一個變量 = 賦值給另一個變量時,不會立即為新變量分配內存空間,而是在原變量的zval中給refcount加1。 只有當原變量或者發生改變時,才會為新變量分配內存空間,同時原變量的refcount減 1 。當然,如果unset原變量,新變量直接就使用原變量的zval而不是重新分配。&引用賦值時,原變量的is_ref 加1. 如果給一個變量&賦值,之前 = 賦值的變量會分配空間。
<?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
五、PHP7.X 標量在zval容器例子
<?php $a = 1; xdebug_debug_zval('a'); echo PHP_EOL; $b = $a; xdebug_debug_zval('a');
結果如下:可以看到標量(布爾,字符串,整形,浮點型)不再計數了
六、PHP5.3復合類型數組和對象在zval容器例子
<?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');
結果如下:可以看出,數組用了比數組長度多1個zval存儲。數組分配了三個zval容器:a meaning number
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
七、PHP7.X復合類型數組和對象在zval容器例子
<?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=2,后經測試發現數組的refcount都是從2開始的
八、循環引用問題
1、PHP7.1效果
<?php $a = array('life'); xdebug_debug_zval( 'a' ); echo PHP_EOL; $a[] = &$a; xdebug_debug_zval('a');
可以看到,箭頭方向表示的就是遞歸循環引用了
2、再看看5.3的效果
說明:在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變量空間是否能夠被釋放的時候是依據這個變量的zval的refcount的值,
如果refcount為0,那么變量的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。現在unset ($a),那么array的refcount減1變為1.現在無任何變量指向這個zval,
而且這個zval的計數器為1,不會回收。 結果:盡管不再有某個作用域中的任何符號指向這個結構(就是變量容器),由於子元素“1”仍然指向數組本身,所以這個容器不能被清除 。
因為沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會導致內存泄漏。 在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()開啟和關閉。
九、垃圾回收算法
1、對每個根緩沖區中的根zval按照深度優先遍歷算法遍歷所有能遍歷到的zval,並將每個zval的refcount減1,同時為了避免對同一zval多次減1(因為可能不同的根能遍歷到同一個zval),
每次對某個zval減1后就對其標記為“已減”。 2、再次對每個緩沖區中的根zval深度優先遍歷,如果某個zval的refcount不為0,則對其加1,否則保持其為0。 3、清空根緩沖區中的所有根(注意是把這些zval從緩沖區中清除而不是銷毀它們),然后銷毀所有refcount為0的zval,並收回其內存。 如果不能完全理解也沒有關系,只需記住PHP5.3的垃圾回收算法有以下幾點特性: 1、並不是每次refcount減少時都進入回收周期,只有根緩沖區滿額后在開始垃圾回收。 2、可以解決循環引用問題。 3、可以總將內存泄露保持在一個閾值以下。