在PHP中,一個變量被指派,内部到底經曆了怎樣的邏輯判斷呢?
PHP在核心中是通過zval這個結構體來存儲變量的,它的定義在Zend/zend.h檔案裡
struct _zval_struct {zvalue_value value; /* 變量的值 */zend_uint refcount__gc;zend_uchar type; /* 變量目前的資料類型 */zend_uchar is_ref__gc;};typedef struct _zval_struct zval;//在Zend/zend_types.h裡定義的:typedef unsigned int zend_uint;typedef unsigned char zend_uchar;
複制
使用xdebug的xdebug_debug_zval函數可以列印出變量的refcount,is_ref的值。
$a = 'Hello World';$b = $a;
複制
以上内容在核心中怎麼執行呢?
zval *helloval;MAKE_STD_ZVAL(helloval);ZVAL_STRING(helloval, "Hello World", 1);zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);ZVAL_ADDREF(helloval); //這句很特殊,我們顯式的增加了helloval結構體的refcountzend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
複制
可以看出來,當變量指派的時候,其實兩個變量指向的是同一個位址空間。那麼問題來了,如果指向同一個位址空間,那不是修改a,b也會跟着改變。這就涉及php的寫時複制機制。 以上代碼,如果後面一行為
$b = '123'
判斷過程如下:
- 如果這個變量的zval部分的refcount小于2,代表沒有别的變量在用,則直接修改這個值
- 否則,複制一份zval 的值,減少原zval的refcount的值,初始化新的zval的refcount,修改新複制的zval
簡單變量
先引用指派後普通指派
var_dump(memory_get_usage());$a = '1234567890';xdebug_debug_zval('a');var_dump(memory_get_usage());$b = &$a;xdebug_debug_zval('a','b');var_dump(memory_get_usage());$c = $a;xdebug_debug_zval('a','b','c');var_dump(memory_get_usage());$a = '1234567890';var_dump(memory_get_usage());$b = &$a;var_dump(memory_get_usage());$c = $a;
複制
輸出内容如下:
int(121672)
a: (refcount=1, is_ref=0)='1234567890'int(121776)
a: (refcount=2, is_ref=1)='1234567890'
b: (refcount=2, is_ref=1)='1234567890'int(121824)
a: (refcount=2, is_ref=1)='1234567890'
b: (refcount=2, is_ref=1)='1234567890'
c: (refcount=1, is_ref=0)='1234567890'int(121928)
複制
$a 指派,開辟了104byte空間,變量a refcount=1,is_ref=0
$b 指派,開辟了48byte空間,變量a refcount=2,is_ref=1。48byte是符号表占用,a,b執行同一個位址空間
$c 指派,開辟了104byte空間。由于a,b是引用,是以在c指派的時候,會開辟新空間,複制a zval内容,并初始化refcount,is_ref,是以a 的refcount不變,c 的refcount=1
先普通指派後引用指派
var_dump(memory_get_usage());$a = '1234567890';xdebug_debug_zval('a');var_dump(memory_get_usage());$b = $a;xdebug_debug_zval('a','b');var_dump(memory_get_usage());$c = &$a;xdebug_debug_zval('a','b','c');var_dump(memory_get_usage());
複制
輸出内容如下:
int(121672)a: (refcount=1, is_ref=0)='1234567890'
int(121776)a: (refcount=2, is_ref=0)='1234567890'
b: (refcount=2, is_ref=0)='1234567890'
int(121824)a: (refcount=2, is_ref=1)='1234567890'
b: (refcount=1, is_ref=0)='1234567890'
c: (refcount=2, is_ref=1)='1234567890'
int(121928)
複制
$a 指派,開辟了104byte空間,變量a refcount=1,is_ref=0
$b 指派,開辟了48byte空間,變量a refcount=2,is_ref=1。48byte是符号表占用,a,b指向同一個位址空間
$c 指派,開辟了104byte空間。由于a,c是引用,需要與b隔離開來,是以會指派原有的zval,初始化zval,将a,c指向新複制的zval,同時原有的zval refcount-1
數組
$arr = [0=>'one'];
xdebug_debug_zval('arr');
$arr[1] = $arr;xdebug_debug_zval('arr');$arr[2] = $arr;
xdebug_debug_zval('arr');
unset($arr[1]);
xdebug_debug_zval('arr');
unset($arr[2]);
xdebug_debug_zval('arr');
複制
輸出内容如下:
arr: (refcount=1, is_ref=0)=array ( 0 => (refcount=1, is_ref=0)='one')
)
arr: (refcount=1, is_ref=0)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=1, is_ref=0)=array (
0 => (refcount=2, is_ref=0)='one'
)
)
arr: (refcount=1, is_ref=0)=array (
0 => (refcount=3, is_ref=0)='one',
1 => (refcount=2, is_ref=0)=array (
0 => (refcount=3, is_ref=0)='one'),
2 => (refcount=1, is_ref=0)=array (
0 => (refcount=3, is_ref=0)='one',
1 => (refcount=2, is_ref=0)=array (...)
)
)
arr: (refcount=1, is_ref=0)=array (
0 => (refcount=3, is_ref=0)='one',
2 => (refcount=1, is_ref=0)=array (
0 => (refcount=3, is_ref=0)='one',
1 => (refcount=1, is_ref=0)=array (...)
)
)
arr: (refcount=1, is_ref=0)=array (
0 => (refcount=1, is_ref=0)='one'
)
複制
$arr = [0=>'one'];xdebug_debug_zval('arr');$arr[1] = &$arr;xdebug_debug_zval('arr');$arr[2] = $arr;xdebug_debug_zval('arr');unset($arr[1]);xdebug_debug_zval('arr');unset($arr[2]);xdebug_debug_zval('arr');
複制
輸出内容如下:
arr: (refcount=1, is_ref=0)=array (
0 => (refcount=1, is_ref=0)='one'
)
arr: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
arr: (refcount=3, is_ref=1)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=3, is_ref=1)=...,
2 => (refcount=2, is_ref=0)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=3, is_ref=1)=...,
2 => (refcount=2, is_ref=0)=...)
)
arr: (refcount=2, is_ref=1)=array (
0 => (refcount=2, is_ref=0)='one',
2 => (refcount=2, is_ref=0)=array (
0 => (refcount=2, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...,
2 => (refcount=2, is_ref=0)=...)
)
arr: (refcount=2, is_ref=1)=array (
0 => (refcount=2, is_ref=0)='one'
)
複制
上面段測試代碼很相似,差别隻在arr[1]是否是引用指派。
arr[1]非引用指派的情況,arr[0]的refcount = 指派次數+1,執行兩次unset之後,arr,arr[0]的refcount都跟開始定義的時候一緻。 arr[1]引用指派的情況,arr[0]的refcount = 非引用指派次數+1,執行兩次unset之後,arr,arr[0] 的refcount都無法回到定義的時候的值。
主要原因在于arr[1]引用指派,構成一個遞歸操作。 但是如果,至于這個refcount,真的說不明白。當沒有arr[2]指派的時候,執行unset, arr refcount能回到1 。從下面這張圖更加清晰看出内部遞歸引用

當出現上面這種情況,refcount本該=1,但實際上面沒有被設定為1,這種情況就會出現記憶體洩漏。上面代碼循環執行100次,記憶體從一開始121096 上升到169224,記憶體占用上升了5k 。
對象
$user = new User();
$m = $user;
$user->user ='';
$user->name = 'sdfsdfs';
xdebug_debug_zval('user','m');
複制
以上内容輸出
(refcount=2, is_ref=0)=class User {
public $name = (refcount=1, is_ref=0)='sdfsdfs';
public $model = (refcount=1, is_ref=0)=NULL;
public $user = (refcount=1, is_ref=0)=''
}
m: (refcount=2, is_ref=0)=class User {
public $name = (refcount=1, is_ref=0)='sdfsdfs';
public $model = (refcount=1, is_ref=0)=NULL;
public $user = (refcount=1, is_ref=0)=''
}
複制
xdebug給出的is_ref=0。refcount與普通變量一直。但是類的指派是引用指派。
$user = new User();
$user->user = $user;
$user->name = 'sdfsdfs';
xdebug_debug_zval('user');
unset($user);
複制
上面内容輸出:
user: (refcount=2, is_ref=0)=class User { public $name = (refcount=1, is_ref=0)='sdfsdfs'; public $user = (refcount=2, is_ref=0)=... }
複制
這裡由于類的指派是引用指派,索引也構成了一個遞歸操作,這樣也會跟數組一樣出現記憶體洩漏的情況。對以下代碼個自行100次
$user = new User();
$user->user = $user;
$user->name = 'sdfsdfs';
xdebug_debug_zval('user');
unset($user);
複制
$user = new User();
$user->user = new Order();
$user->name = 'sdfsdfs';
xdebug_debug_zval('user');
unset($user);
複制
第一段代碼前後記憶體差1408 byte. 第二段代碼差208 byte。