似乎沒有辦法檢查特定類別變數的參考計數,但您可以使用 xdebug_debug_zval('this'); 來檢視目前類別實例中所有變數的參考計數。
一個 PHP 變數儲存在一個稱為「zval」的容器中。一個 zval 容器除了變數的型別和值之外,還包含兩個額外的資訊位元。第一個稱為「is_ref」,是一個布林值,表示變數是否為「參考集」的一部分。透過這個位元,PHP 的引擎知道如何區分一般變數和參考。由於 PHP 允許使用者建立參考(如 & 運算子所建立的參考),zval 容器也有一個內部參考計數機制來最佳化記憶體使用。第二個額外資訊,稱為「refcount」,包含有多少個變數名稱(也稱為符號)指向這個 zval 容器。所有的符號都儲存在符號表中,每個作用域都有一個符號表。主腳本(即透過瀏覽器請求的腳本)有一個作用域,每個函數或方法也有一個作用域。
當使用常數值建立新變數時,就會建立一個 zval 容器,例如
範例 #1 建立新的 zval 容器
<?php
$a = "new string";
?>
在這個情況下,新的符號名稱 a
在目前作用域中建立,並建立一個新的變數容器,其型別為 字串,值為 new string
。「is_ref」位元預設設定為 false
,因為沒有建立使用者參考。「refcount」設定為 1
,因為只有一個符號使用這個變數容器。請注意,參考(即「is_ref」為 true
)且「refcount」為 1
時,會被視為非參考(即「is_ref」為 false
)。如果您已安裝 » Xdebug,您可以呼叫 xdebug_debug_zval() 來顯示此資訊。
範例 #2 顯示 zval 資訊
<?php
$a = "new string";
xdebug_debug_zval('a');
?>
上面的範例會輸出
a: (refcount=1, is_ref=0)='new string'
將這個變數指派給另一個變數名稱會增加 refcount。
範例 #3 增加 zval 的 refcount
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>
上面的範例會輸出
a: (refcount=2, is_ref=0)='new string'
此處的 refcount 為 2
,因為相同的變數容器同時連結到 a 和 b。當不需要複製實際的變數容器時,PHP 會聰明地不進行複製。當「refcount」達到零時,變數容器就會被銷毀。當連結到變數容器的任何符號離開作用域(例如,當函數結束時)或當符號被取消指派時(例如,透過呼叫 unset()),「refcount」會減少一。以下範例顯示了這一點
範例 #4 減少 zval refcount
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );
?>
上面的範例會輸出
a: (refcount=3, is_ref=0)='new string' a: (refcount=2, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
如果我們現在呼叫 unset($a);
,變數容器(包括型別和值)將會從記憶體中移除。
對於複合型別(如 陣列和 物件),情況會稍微複雜一些。與 純量 值不同,陣列和 物件將其屬性儲存在它們自己的符號表中。這表示以下範例會建立三個 zval 容器
範例 #5 建立一個 陣列 zval
<?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 容器為:a、meaning 和 number。類似的規則適用於增加和減少「refcount」。以下,我們將另一個元素加入陣列,並將其值設定為現有元素的值
範例 #6 將已存在的元素加入陣列
<?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' )
或以圖形表示
從上面的 Xdebug 輸出中,我們看到舊的和新的陣列元素現在都指向一個「refcount」為 2
的 zval 容器。雖然 Xdebug 的輸出顯示了兩個值為 'life'
的 zval 容器,但它們是同一個。 xdebug_debug_zval() 函數並未顯示此資訊,但您可以透過同時顯示記憶體指標來查看它。
從陣列中移除元素就像從作用域中移除符號一樣。這樣做會減少陣列元素指向的容器的「refcount」。同樣地,當「refcount」達到零時,變數容器會從記憶體中移除。以下是一個範例來說明這一點
範例 #7 從陣列中移除元素
<?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' )
現在,如果我們將陣列本身新增為陣列的元素,事情會變得有趣。在下一個範例中,我們也會偷偷加入一個參考運算子,因為否則 PHP 會建立一個副本。
範例 #8 將陣列新增為自身的元素
<?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)以及第二個元素(1)現在都指向一個具有「refcount」為 2
的變數容器。 上面顯示中的「...」表示有遞迴存在,當然,在這種情況下,「...」表示指向原始陣列。
就像之前一樣,取消設定變數會移除符號,並且它指向的變數容器的參考計數會減少 1。 因此,如果在執行上述程式碼後取消設定變數 $a,則 $a 和元素「1」指向的變數容器的參考計數將從「2」減少到「1」。 這可以表示為
範例 #9 取消設定 $a
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )
或以圖形表示
雖然在任何範圍中都沒有符號指向此結構,但它無法清理,因為陣列元素「1」仍然指向同一個陣列。 因為沒有外部符號指向它,使用者無法清理此結構; 因此,您會發生記憶體洩漏。 幸運的是,PHP 會在請求結束時清理此資料結構,但在那之前,它會佔用寶貴的記憶體空間。 如果您正在實作剖析演算法或其他讓子項目指向「父」元素的項目,則經常會發生這種情況。 當然,物件也會發生相同的情況,而且實際上更可能發生,因為物件總是隱式地以「參考」方式使用。
如果這種情況只發生一兩次,可能不是問題,但如果有成千上萬甚至數百萬的這些記憶體損失,這顯然會成為一個問題。 這在長時間執行的腳本(例如永遠不會結束請求的守護程序)或大量的單元測試中尤其有問題。 後者在執行 eZ Components 程式庫的 Template 元件的單元測試時造成了問題。 在某些情況下,它需要超過 2 GB 的記憶體,而測試伺服器並沒有那麼多記憶體。
「範例 #8 將陣列新增為自身的元素」的結果在 PHP7 中會有所不同
a: (refcount=2, is_ref=1)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
而不是
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
PHP 7 中的內部值表示
https://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html
$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
使用 PHP 7.3.12 (cli) 輸出
a: (interned, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1
我的 PHP 版本:HP 7.1.25 (cli) (built: Dec 7 2018 08:20:45) ( NTS )
$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
輸出
a: (refcount=2, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1
如果 $a 是字串值,則 'refcount' 預設等於 2。
我的 PHP 版本是 PHP 7.1.6 (cli),當我執行
$a = 'new string';
$b = 1;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
它顯示
a: (refcount=0, is_ref=0)='new string'
b: (refcount=0, is_ref=0)=1