PHP-----淺談垃圾回收機制


前言

大多數編程語言都會有自身的垃圾回收機制,php也不例外。經常聽很多人說gc,也就是垃圾回收器,全程為Garbage Collection。

在php5.3之前,是不包括垃圾回收機制的,也沒有專門的垃圾回收器,實現垃圾回收就是簡單判斷一下變量的zval的refcount是否為0,是的話就釋放。

但是如果這么簡單的判斷垃圾回收的話,很容易引起程序過程中內存溢出。如果存在"自身指向自身"的情況的話,那么變量將無法回收早成內存泄露,所以從php5.3開始就出現了專門負責清理垃圾數據防止內存泄露的垃圾回收器。

 

引用計數的基本知識

我們要了解GC,那么首先要了解引起垃圾回收的基數是什么。

在php中,每個變量存在一個叫“zval”的變量容器中。一個zval變量容器,除了包含變量的類型和值,還包括另外兩個字節的額外信息。第一個是"is_ref"。第二個是"refcount"。

is_ref是一個布爾類型的值,用來標示這個變量是否屬於引用集合。通過這個字節,php引擎才能把普通變量和引用變量區分開來,由於php允許用戶通過"&"來使用自定義的引用,所以zval中還有一個內部引用計數機制,來進行優化內存。

refcount用來表示這個zval變量容器的變量的個數。所有符號存在一個符號表當中,每個符號都有作用域。

通俗的講:

  • refcount就是多少個變量是一樣的用了相同的值,那么refcount就是這個值
  • is_ref就是當有變量用了&的形式進行賦值,那么is_ref的值就會增加
<?php
$a = "new string";
?>
在上面的代碼中,變量a是在當前作用於中生成的,並且生成了類型為String和值為"new string"的變量容器。這個時候is_ref被默認的設置成了false,因為現在沒有任何自定義的引用生成。refcount被設置成了1。我們可以用php來看到這些計數的變化,首先需要用到xdebug,所以php沒有裝上xdebug擴展的需要先裝一下。
<?php
$a = "new string";
xdebug_debug_zval('a');

輸出:a: (refcount=1, is_ref=0)='new string'
?>
增加zval的引用計數
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );

輸出:a: (refcount=2, is_ref=0)='new string'
?>
這時的引用次數是2,因為同一個變量容器被變量 a 和變量 b關聯.當沒必要時,php不會去復制已生成的變量容器。變量容器在”refcount“變成0時就被銷毀. 當任何關聯到某個變量容器的變量離開它的作用域(比如:函數執行結束),或者對變量調用了函數 unset()時,”refcount“就會減1。

減少引用計數

<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );

輸出:
a: (refcount=3, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'
?>
執行 unset($a);,包含類型和值的這個變量容器就會從內存中刪除。

復合類型

當變量的類型為array或object這樣的復合類型時,array和object類型的變量把他們的成員或屬性存在自己的符號表中。

<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );

輸出:
a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)
?>
根據上面的代碼,我們可以理解,對於數組來看成一個整體,對於內部的值來看又是一個獨立的整體,各自都有着一套zval的refcount和is_ref。下面這張圖是從官網上扒下來的:

添加一個已經存在的元素到數組中:

<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );

輸出:
a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)
?>
如下圖解釋:

從數組中刪除一個元素:

<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );

輸出:
a: (refcount=1, is_ref=0)=array (
   'life' => (refcount=1, is_ref=0)='life'
)
?>
刪除數組中一個元素,就是類似從作用於中刪除一個變量,刪除后數組中這個元素所在容器的refcount的值減少,當refcount為0時,這個變量容器就從內存中被刪除。 

將數組作為一個元素添加給自身:

<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );

輸出:
a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...
)
?>

我們可以看到數組a同時也是這個數組第二個元素,指向的變量容器中refcount的值為2,上面輸出的“...”說明發生了遞歸操作,意味着"..."指向原始數組。

盡管不再有某個作用域中的任何符號指向這個結構(就是變量容器),由於數組元素“1”仍然指向數組本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會導致內存泄漏。

 

垃圾回收周期

在5.3之前的版本中,php無法處理循環的引用內存泄露。但是自5.3之后php使用引用計數系統中同步周期回收的同步算法,僅處理這個內存泄露問題。

基本思想是如果一個引用計數增加那么將繼續被使用,當然就不再是垃圾。如果引用計數減少到零,所在變量容器將被清除。那么也就是說只有在引用計數減少到非零值時,才會產生垃圾周期。在一個垃圾周期中通過檢查引用計數是否減1,並且檢查哪些變量容器的引用次數為零,來發現哪些是垃圾。

我們就拿這張圖舉例(來自php官網)。為了避免不得不檢查所有引用計數可能減少的垃圾周期,同步算法將所有可能根放在了根緩沖區(root buffer)中(在圖中用紫色來標記,稱為疑似垃圾),這樣可以同時確保每個可能的垃圾根在緩沖區中只出現一次。僅當根緩沖區滿了時,才對緩沖區中所有不同的變量容器執行垃圾回收操作,在圖中體現為步驟A。

在步驟B中,模擬刪除每個紫色的變量。模擬刪除時可能將不是紫色的不同變量引用數減1,如果某個普通變量引用計數變成0時,就對這個普通變量在做一次模擬刪除。每個變量只能被模擬刪除一次,模擬刪除后標記為灰色。

在步驟C中,模擬恢復每個紫色變量。當然這個恢復是有條件的,當變量的引用計數大於0時才對其做模擬恢復。同樣的每個變量只能恢復一次,恢復后標記為黑色,這樣生下一對沒能恢復的就是該刪除的藍色節點了,在步驟D中遍歷出來真正的刪除掉。

在php中垃圾回收機制默認是打開的,在你的php.ini中可以手動設置,通過zend.enable_gc這個屬性進行開啟或關閉垃圾回收機制。當開啟了垃圾回收機制后,每當根緩存區存滿時,就會執行上面描述的循環查找算法。根緩存區具有固定的大小,當然你可以通過修改php源碼文件Zend/zend_gc.c中常量GC_ROOT_BUFFER_MAX_ENTRIES來修改根緩存區的大小(注意修改后需要重新編譯php)。當關閉垃圾回收機制后,這個循環查找算法將不會執行,然而可能根會一直存在於根緩沖區中,不管在配置中是否激活了垃圾回收機制。

當然你也可以通過調用gc_enable()和gc_disable()函數來打開和關閉垃圾回收機制,效果和修改配置項相同。即使根緩沖區還沒有滿,也能強制執行周期回收。

 

php的內存管理機制

現在我們已經知道了zval是怎么回事了。那么現在我們需要知道php的內存管理機制是怎么一回事。

var_dump(memory_get_usage());
$test = "這是測試啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());

輸出(php5.6):
/var/www/html/node_test/phptest/phptest.php:51:
int(361896)
/var/www/html/node_test/phptest/phptest.php:53:
int(361928)
/var/www/html/node_test/phptest/phptest.php:55:
int(361896)
過程是:定義變量->內存增加->清除變量->內存恢復
var_dump(memory_get_usage());
$test = "這是測試啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());

輸出(php7.1):
/var/www/html/node_test/phptest/phptest.php:51:
int(361896)
/var/www/html/node_test/phptest/phptest.php:53:
int(361928)
/var/www/html/node_test/phptest/phptest.php:55:
int(361928)
而我在用php7時發現了這個問題,這就要說道php5和php7的內存管理機制和垃圾回收機制的不同了,這里暫且不表。我們繼續往下走。

當在執行

$test = "這是測試啊";
內存的分配做了兩件事:
  1. 為變量名分配內存,並存入符號表
  2. 為變量值分配內存

我們再看代碼:

var_dump(memory_get_usage());
for($i=0;$i<100;$i++)
{
    $a = "test".$i;
    $$a = "hello";    
}
var_dump(memory_get_usage());
for($i=0;$i<100;$i++)
{
    $a = "test".$i;
     unset($$a);    
}
var_dump(memory_get_usage());

輸出:
/var/www/html/node_test/phptest/phptest.php:57:
int(363520)
/var/www/html/node_test/phptest/phptest.php:63:
int(372384)
/var/www/html/node_test/phptest/phptest.php:69:
int(369216)
為什么內存沒有全部收回來呢?

因為php的核心結構Hashtable,在定義的時候不可能一次性分配足夠多的內存塊,所以初始化的時候只會分配一小塊,等不夠的時候在進行擴容,而Hashtable只擴容不減少,所以當存入100個變量的時候符號表不夠用了就進行一次擴容,當unset()時只是放了為變量值分配的內存,但是為變量名分配的內存還是在符號表中的,符號表並沒有縮小,所以沒收回來的內存是被符號表占去了。

php並不是只要內存不夠就去向OS申請內存,而是先申請一大塊內存,然后將其中一部分分給申請者,這樣再有邏輯需要申請內存的時候,就不需要再向OS申請內存了,避免了重復申請,只有當一大塊內存不夠用的時候再去申請。而當釋放內存時,php並非把內存還給了OS,而是把內存軌道自己維護的空閑內存列表,以便重復利用。

新版本的php(5.3版本之后)是如何處理垃圾內存的?

剛剛上面我們已經講了,針對在php中環形引用導致的垃圾,產生了新的同步算法(GC算法),對於官網上的理論,我進行了理解:

如果一個zval的refcount增加,那么表明該變量的zval還在使用,不屬於垃圾

如果一個zval的refcount減少到0,那么zval可以被釋放掉,可以清除,不是垃圾

如果在經過模擬刪除后一個zval的refcount減1,如果該zval的引用次數為是大於0,那么此zval不能被釋放,可能是一個垃圾

 

關於垃圾回收的小知識點

unset():unset()只是斷開一個變量到一塊內存區域的連接,同時將該內存區域的引用計數減1,內存是否回收主要還是看refcount是否到0了。

null:將null賦值給一個變量是直接將該變量指向的數據結構置空,同時將其引用計數歸0。

腳本執行結束:該腳本中所有內存都會被釋放,無論是否有環引用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM