今天看php發(fā)展歷史堤尾,了解到在php4的年代芋绸,對象的賦值默認(rèn)是創(chuàng)建副本媒殉,并非是增加對象的引用。就試了下在php5的環(huán)境下摔敛,怎樣將對象一般賦值廷蓉、引用賦值和拷貝。然后在sf里看到有人提了這么個問題:
class User
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
public function __destruct()
{
echo 'I was released~~';
}
}
$instance = new User('xiao ming');
$assignment = $instance;
$reference = &$instance;
$instance = null;
var_dump($instance); // 輸出 null
var_dump($assignment); // public 'name' => string 'xiao ming' (length=9)
var_dump($reference); // 輸出 null
發(fā)現(xiàn)這個問題挺有意思马昙,就查了下php手冊桃犬,將引用和垃圾回收機制了解了下,才明白這些代碼背后發(fā)生了什么行楞。
先解釋下為什么$instance置為null后攒暇,$reference也變成null了。$reference是對$instance的引用子房,php官方文檔是這樣描述引用的:
References in PHP are a means to access the same variable content by different names. They are not like C pointers; for instance, you cannot perform pointer arithmetic using them, they are not actual memory addresses, and so on. Instead, they are symbol table aliases. Note that in PHP, variable name and variable content are different, so the same content can have different names.
從這段話我們就明白了形用,$instance存著指向小明對象的指針的值(這個后面詳細講)就轧,而$reference單純的只是$instance的另一個別名而已,并不像C那樣是實例的指針田度。因此$instance置為null妒御,釋放了對user對象的引用后,$reference也自然無法access the same variable了镇饺。
而$assignment還能同樣輸出小明這個對象乎莉,是因為$assignment存的內(nèi)容和$instance一樣,都是這個對象的內(nèi)存地址兰怠。這就又有一個問題了,這個小明對象在什么時候會被釋放呢李茫?
A PHP variable is stored in a container called a "zval". A zval container contains, besides the variable's type and value, two additional bits of information. The first is called "is_ref" and is a boolean value indicating whether or not the variable is part of a "reference set". This second piece of additional information, called "refcount", contains how many variable names (also called symbols) point to this one zval container.
這下我們明白了揭保,PHP使用了引用計數(shù)(reference count)垃圾回收機制。每個對象都內(nèi)含一個引用計數(shù)器魄宏,當(dāng)有reference連接到對象時秸侣,計數(shù)器加1。當(dāng)reference離開生存空間或被設(shè)為null時宠互,計數(shù)器減1味榛。當(dāng)某個對象的引用計數(shù)器為零時,PHP知道你將不再使用這個對象予跌,釋放其所占的內(nèi)存空間搏色。我們將上邊的代碼稍作修改,在$instance = null前加幾句話(ps:需要裝xdebug)
$instance = new User('xiao ming');
$assignment = $instance;
$reference = &$instance;
//php5輸出結(jié)果
xdebug_debug_zval('instance'); // instance: (refcount=2, is_ref=1)
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference'); // reference: (refcount=2, is_ref=1)
// php7輸出結(jié)果
xdebug_debug_zval('instance'); // instance: (refcount=2, is_ref=1)
xdebug_debug_zval('assignment'); // assignment:(refcount=2, is_ref=0)
xdebug_debug_zval('reference'); // reference: (refcount=2, is_ref=1)
$instance = null;
$assignment = null; // 這句話執(zhí)行后立即觸發(fā)User類的析構(gòu)函數(shù)券册,輸出 I was released~~
由于家里和公司的電腦環(huán)境一個是php5.6频轿,另個一是php7.1,同樣的demo中突然發(fā)現(xiàn)php5和php7里assignment的refcount值不一樣烁焙,就去看了下php對于變量的實現(xiàn)方式航邢,由于我的C功底實在有點差,源碼看的很吃力骄蝇,不足的地方還望各位能多多指教膳殷。先看下在php5和php7里對象賦值時的不同處理方式(簡化后的模型):
若在變量輸出前執(zhí)行unset($instance)和unset($reference),就更容易看出上圖所示不同之處了九火。
$instance = new User('xiao ming');
$assignment = $instance;
$reference = &$instance;
unset($instance);
//php5輸出結(jié)果
xdebug_debug_zval('instance'); // instance: no such symbol
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference'); // reference: (refcount=1, is_ref=0)
// php7輸出結(jié)果
xdebug_debug_zval('instance'); // instance: uninitialized
xdebug_debug_zval('assignment'); // assignment:(refcount=2, is_ref=0)
xdebug_debug_zval('reference'); // reference: (refcount=1, is_ref=1)
unset($reference);
//php5輸出結(jié)果
xdebug_debug_zval('instance'); // instance: no such symbol
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference'); // reference: no such symbol
// php7輸出結(jié)果
xdebug_debug_zval('instance'); // instance: uninitialized
xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0)
xdebug_debug_zval('reference'); // reference: uninitialized
// unset的作用僅僅是將變量從符號表中刪除赚窃,并減少所指結(jié)構(gòu)體的refcount值。
現(xiàn)在來詳細談下php5和php7里zval的不同之處岔激。
php7的zend_object如下:
struct _zend_object {
zend_refcounted gc;
uint32_t handle;
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
可以看出考榨,php7中的zval已經(jīng)變成了一個值指針,它要么保存著原始值鹦倚,要么保存著一個指針(索引)handle河质,這個索引對應(yīng)著一個保存原始對象的指針數(shù)組中某個元素的地址。也就是說,當(dāng)對象被創(chuàng)建時掀鹅,會有一個指針插入到對象存儲中并且其索引會保存在 handle 中散休,當(dāng)對象被釋放時,索引也會被移除乐尊。
那么php7為什么還要隔一層handle去訪問對象真正的內(nèi)容呢戚丸,而不是直接存著內(nèi)容的地址。The reason behind this is that during request shutdown, there comes a point where it is no longer safe to run userland code, because the executor is already partially shut down. To avoid this PHP will run all object destructors at an early point during shutdown and prevent them from running at a later point in time. For this a list of all active objects is needed.<摘自https://nikic.github.io/2015/06/19/Internal-value-representation-in-PHP-7-part-2.html扔嵌,作者是php官方開發(fā)組成員Nikita Popov>
并且 handle 對于調(diào)試也是很有用的限府,它讓每個對象都有了一個唯一的 ID,這樣就很容易區(qū)分兩個對象是同一個還是只是有相同的內(nèi)容痢缎。雖然 HHVM 沒有對象存儲的概念胁勺,但它也存了對象的 handle。
若將$assignment = $instance;改成$assignment = clone $instance;會發(fā)現(xiàn)assignment的ID將變?yōu)?
php5中取對象就比較麻煩了独旷。php5的zend_object_value結(jié)構(gòu)如下
typedef struct _zend_object_value {
zend_object_handle handle;
const zend_object_handlers *handlers;
} zend_object_value;
對象句柄(handler)是對象的唯一ID署穗,作為索引用于“對象存儲”,對象存儲本身是一個存儲容器(bucket)的數(shù)組嵌洼,bucket 定義如下:
typedef struct _zend_object_store_bucket {
zend_bool destructor_called;
zend_bool valid;
zend_uchar apply_count;
union _store_bucket {
struct _store_object {
void *object;
zend_objects_store_dtor_t dtor;
zend_objects_free_object_storage_t free_storage;
zend_objects_store_clone_t clone;
const zend_object_handlers *handlers;
zend_uint refcount;
gc_root_buffer *buffered;
} obj;
struct {
int next;
} free_list;
} bucket;
} zend_object_store_bucket;
第一個成員 object 是指向?qū)嶋H對象的指針案疲,現(xiàn)在看下object所指向的結(jié)構(gòu)體,對于普通用戶區(qū)所定義的object內(nèi)容通常如下:
typedef struct _zend_object {
zend_class_entry *ce;
HashTable *properties;
zval **properties_table;
HashTable *guards;
} zend_object;
現(xiàn)在我們要從對象 zval 中取出一個元素麻养,先需要取出the object store bucket褐啡,然后是 zend object,然后才能通過指針找到對象屬性表和 zval鳖昌。這樣這里至少就有 4 層間接訪問(并且實際使用中可能最少需要7層)春贸。
簡單點說,在php7中的zval遗遵,相當(dāng)于php5的zval*萍恕。只不過相對于zval*,直接存儲zval车要,我們可以省掉一次指針解引用允粤。
總結(jié)一下,php7新的zval設(shè)計不再單獨從堆上分配內(nèi)存并且不自己存儲引用計數(shù)翼岁。需要使用 zval 指針的復(fù)雜類型(比如字符串类垫、數(shù)組和對象)會自己存儲引用計數(shù)(php5除了zval本身有引用計數(shù)以外,bucket也存儲了refcount)琅坡。這樣就可以有更少的內(nèi)存分配悉患、更少的間接指針以及更少的內(nèi)存使用。
補充:
1榆俺、php7中簡單數(shù)據(jù)類型不再單獨分配內(nèi)存售躁,也不再計數(shù)坞淮。所以會有以下情況:
$a = 1;
// php5輸出結(jié)果
xdebug_debug_zval('a); // (refcount=1, is_ref=0)
// php7輸出結(jié)果
xdebug_debug_zval('a); // (refcount=0, is_ref=0)
2、php5中refcount等于1時陪捷,is_ref永遠等于0回窘;php7則不會
版權(quán)聲明:本文為原創(chuàng)文章,轉(zhuǎn)載請注明出處市袖,謝謝~~