PHP的引用計數是什麼意思?

else發表於2021-09-09

什麼是引用計數

在PHP的資料結構中,引用計數就是指每一個變數,除了儲存了它們的型別和值之外,還額外儲存了兩個內容,一個是當前這個變數是否被引用,另一個是引用的次數。為什麼要多儲存這樣兩個內容呢?當然是為了垃圾回收(GC)。也就是說,當引用次數為0的時候,這個變數就沒有再被使用了,就可以透過 GC 來進行回收,釋放佔用的記憶體資源。任何程式都不能無限制的一直佔用著記憶體資源,過大的記憶體佔用往往會帶來一個嚴重的問題,那就是記憶體洩露,而 GC 就是PHP底層自動幫我們完成了記憶體的銷燬,而不用像 C 一樣必須去手動地 free 。

怎麼檢視引用計數?

我們需要安裝 xdebug 擴充套件,然後使用 xdebug_debug_zval() 函式就可以看到指定記憶體的詳細資訊了,比如:

$a = "I am a String";
xdebug_debug_zval('a');
// a: (refcount=1, is_ref=0)='I am a String'

從上述內容中可以看出,這個 $a 變數的內容是 I am a String 這樣一個字串。而括號中的 refcount 就是引用次數,is_ref 則是說明這個變數是否被引用。我們透過變數賦值來看看這個兩個引數是如何變化的。

$b = $a;
xdebug_debug_zval('a');
// a: (refcount=1, is_ref=0)='I am a String'

$b = &$a;
xdebug_debug_zval('a');
// a: (refcount=2, is_ref=1)='I am a String'

當我們進行普通賦值後,refcount 和 is_ref 沒有任何變化,但當我們進行引用賦值後,可以看到 refcount 變成了2,is_ref 變成了1。這也就是說明當前的 $a 變數被引用賦值了,它的記憶體符號表服務於 $a 和 $b 兩個變數。

$c = &$a;
xdebug_debug_zval('a');
// a: (refcount=3, is_ref=1)='I am a String'

unset($c, $b);
xdebug_debug_zval('a');
// a: (refcount=1, is_ref=1)='I am a String'

$b = &$a;
$c = &$a;
$b = "I am a String new";
xdebug_debug_zval('a');
// a: (refcount=3, is_ref=1)='I am a String new'

unset($a);
xdebug_debug_zval('a');
// a: no such symbol

繼續增加一個 $c 的引用賦值,可以看到 refcount 會繼續增加。然後 unset 掉 $b 和 $c 之後,refcount 恢復到了1,不過這時需要注意的是,is_ref 依然還是1,也就是說,這個變數被引用過,這個 is_ref 就會變成1,即使引用的變數都已經 unset 掉了這個值依然不變。

最後我們 unset 掉 $a ,顯示的就是 no such symbol 了。當前變數已經被銷燬不是一個可以用的符號引用了。(注意,PHP中的變數對應的是記憶體的符號表,並不是真正的記憶體地址)

物件的引用計數

和普通型別的變數一樣,物件變數也是使用同樣的計數規則。

// 物件引用計數
class A{

}
$objA = new A();
xdebug_debug_zval('objA');
// objA: (refcount=1, is_ref=0)=class A {  }

$objB = $objA;
xdebug_debug_zval('objA');
// objA: (refcount=2, is_ref=0)=class A {  }

$objC = $objA;
xdebug_debug_zval('objA');
// objA: (refcount=3, is_ref=0)=class A {  }

unset($objB);
class C{

}
$objC = new C;
xdebug_debug_zval('objA');
// objA: (refcount=1, is_ref=0)=class A {  }

不過這裡需要注意的是,物件的符號表是建立的連線,也就是說,對 $objC 進行重新例項化或者修改為 NULL ,並不會影響 $objA 的內容,這方面的知識我們在之前的 [物件賦值在PHP中到底是不是引用?]文章中已經有過說明。物件進行普通賦值操作也是引用型別的符號表賦值,所以我們不需要加 & 符號。

陣列的引用計數

// 陣列引用計數
$arrA = [
    'a'=>1,
    'b'=>2,
];
xdebug_debug_zval('arrA');
// arrA: (refcount=2, is_ref=0)=array (
//     'a' => (refcount=0, is_ref=0)=1, 
//     'b' => (refcount=0, is_ref=0)=2
// )

$arrB = $arrA;
$arrC = $arrA;
xdebug_debug_zval('arrA');
// arrA: (refcount=4, is_ref=0)=array (
//     'a' => (refcount=0, is_ref=0)=1, 
//     'b' => (refcount=0, is_ref=0)=2
// )

unset($arrB);
$arrC = ['c'=>3];
xdebug_debug_zval('arrA');
// arrA: (refcount=2, is_ref=0)=array (
//     'a' => (refcount=0, is_ref=0)=1, 
//     'b' => (refcount=0, is_ref=0)=2
// )

// 新增一個已經存在的元素
$arrA['c'] = &$arrA['a'];
xdebug_debug_zval('arrA');
// arrA: (refcount=1, is_ref=0)=array (
//     'a' => (refcount=2, is_ref=1)=1, 
//     'b' => (refcount=0, is_ref=0)=2, 
//     'c' => (refcount=2, is_ref=1)=1
// )

除錯陣列的時候,我們會發現兩個比較有意思的事情。

一是陣列內部的每個元素又有單獨的自己的引用計數。這也比較好理解,每一個陣列元素都可以看做是一個單獨的變數,但陣列就是這堆變數的一個雜湊集合。如果在物件中有成員變數的話,也是一樣的效果。當陣列中的某一個元素被 & 引用賦值給其他變數之後,這個元素的 refcount 會增加,不會影響整個陣列的 refcount 。

二是陣列預設上來的 refcount 是2。其實這是 PHP7 之後的一種新的特性,當陣列定義並初始化後,會將這個陣列轉變成一個不可變陣列(immutable array)。為了和普通陣列區分開,這種陣列的 refcount 是從2開始起步的。當我們修改一下這個陣列中的任何元素後,這個陣列就會變回普通陣列,也就是 refcount 會變回1。這個大家可以自己嘗試下,關於為什麼要這樣做的問題,官方的解釋是為了效率,具體的原理可能還是需要深挖 PHP7 的原始碼才能知曉。

關於記憶體洩露需要注意的地方

其實 PHP 在底層已經幫我們做好了 GC 機制就不需要太關心變數的銷燬釋放問題,但是,千萬要注意的是物件或陣列中的元素是可以賦值為自身的,也就是說,給某個元素賦值一個自身的引用就變成了迴圈引用。那麼這個物件就基本不太可能會被 GC 自動銷燬了。

// 物件迴圈引用
class D{
    public $d;
}
$d = new D;
$d->d = $d;
xdebug_debug_zval('d');
// d: (refcount=2, is_ref=0)=class D { 
//     public $d = (refcount=2, is_ref=0)=... 
// }

// 陣列迴圈引用
$arrA['arrA'] = &$arrA;
xdebug_debug_zval('arrA');
// arrA: (refcount=2, is_ref=1)=array (
//     'a' => (refcount=0, is_ref=0)=1, 
//     'b' => (refcount=0, is_ref=0)=2, 
//     'arrA' => (refcount=2, is_ref=1)=...
// )

不管是物件還是陣列,在列印除錯時出現了 … 這樣的省略號,那麼你的程式中就出現了迴圈引用。在之前的文章 [關於PHP中物件複製的那點事兒]中我們也講過這個迴圈引用的問題,所以這個問題應該是我們在日常開發中應該時刻關注的問題。

總結

引用計數是瞭解垃圾回收機制的前提條件,而且正是因為現代語言中都有一套類似的垃圾回收機制才讓我們的程式設計變得更加容易且安全。那麼有人說了,日常開發根本用不到這些呀?用不到不代表不應該去學習,就像迴圈引用這個問題一樣,當程式碼中充斥著大量的類似程式碼時,系統崩潰只是遲早的事情,所以,這些知識是我們向更高階的程式進階所不可或缺的內容。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2318/viewspace-2797849/,如需轉載,請註明出處,否則將追究法律責任。

相關文章